From c4c3ed7711332b61a1cd05d924b2b1ffa1442487 Mon Sep 17 00:00:00 2001 From: emirplus Date: Thu, 8 Oct 2026 19:18:36 +0300 Subject: [PATCH 001/318] fix(savedata): recover from metadata setup failure Report SAVE_DATA_ERROR_INTERNAL when setupSaveDataMemory2 fails to persist initial SaveDataParam metadata, and restore previous state so retrying setup can initialize cleanly. Closes #1667. --- .../libs/prx/libSceSaveData.native/Export.cpp | 11 ++++- core/libs/tests/SaveDataMemoryMetadata.cpp | 43 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index cfa6c5881..7db0d9691 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -633,6 +633,7 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (have && !read_file_all(path, data)) { return SAVE_DATA_ERROR_INTERNAL; } + const std::vector originalData = data; data.resize(setup_param->memory_size, 0); if (!write_file_replace(path, data)) { return SAVE_DATA_ERROR_INTERNAL; @@ -640,7 +641,15 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); - write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd); + if (!write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd)) { + if (!have) { + std::error_code removeError; + std::filesystem::remove(path, removeError); + } else { + write_file_replace(path, originalData); + } + return SAVE_DATA_ERROR_INTERNAL; + } } } if (result != nullptr) { diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index 70724bd18..c92d0f8f4 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -19,6 +19,7 @@ extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); int APS5_VABI sceSaveDataTerminate(); int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2*); +int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2*, SaveDataMemorySetupResult*); } static void Check(bool value, int line) { @@ -115,6 +116,48 @@ int main() { Require(sceSaveDataSetSaveDataMemory2(&set) == 0); std::copy(first.begin(), first.end(), memoryExpected.begin() + 4); Require(Read(memoryPath) == memoryExpected); + const auto setupParamPath = std::filesystem::path("_sd_mem/u42/slot0.param"); + const auto setupBinPath = std::filesystem::path("_sd_mem/u42/slot0.bin"); + const auto setupParamTemp = setupParamPath.string() + ".tmp"; + Require(std::filesystem::create_directories(setupParamPath.parent_path())); + Require(std::filesystem::create_directory(setupParamTemp)); + SaveDataParam setupParam{}; + setupParam.user_param = 100; + SaveDataMemorySetup2 setup2{}; + setup2.user_id = 42; + setup2.slot_id = 0; + setup2.memory_size = 128; + setup2.option = 1; + setup2.init_param = &setupParam; + SaveDataMemorySetupResult setupResult{}; + setupResult.existed_memory_size = 555; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(setupResult.existed_memory_size == 555); + Require(!std::filesystem::exists(setupBinPath)); + Require(!std::filesystem::exists(setupParamPath)); + Require(!std::filesystem::exists(setupParamTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 0); + Require(Read(setupBinPath) == std::vector(128, 0)); + const auto* setupBytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(setupBytes, setupBytes + sizeof(setupParam))); + const auto growthParamTemp = setupParamPath.string() + ".tmp"; + Require(std::filesystem::create_directory(growthParamTemp)); + SaveDataParam previousParam = setupParam; + setupParam.user_param = 200; + setup2.memory_size = 256; + setupResult.existed_memory_size = 999; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(setupResult.existed_memory_size == 999); + Require(Read(setupBinPath) == std::vector(128, 0)); + const auto* previousBytes = reinterpret_cast(&previousParam); + Require(Read(setupParamPath) == std::vector(previousBytes, previousBytes + sizeof(previousParam))); + Require(!std::filesystem::exists(growthParamTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 128); + Require(Read(setupBinPath) == std::vector(256, 0)); + const auto* updatedBytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); std::filesystem::current_path(previous); std::filesystem::remove_all(root); From a8c2100f5904bd9ca8b623c91764f1badbfe5532 Mon Sep 17 00:00:00 2001 From: emirplus Date: Thu, 8 Oct 2026 21:41:09 +0300 Subject: [PATCH 002/318] fix(savedata): ensure metadata precedes blob commit and handle rollback recovery Write initial SaveDataParam metadata before replacing the memory blob so metadata failure cannot leave a prematurely grown blob. Handle blob write failure with metadata rollback, and allow retries to recover slots with missing metadata. Expand tests with nonzero data, blob failure, and fresh readback. --- .../libs/prx/libSceSaveData.native/Export.cpp | 37 ++++++---- core/libs/tests/SaveDataMemoryMetadata.cpp | 67 ++++++++++++++++++- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 7db0d9691..3cc62db23 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -625,32 +625,41 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (!have) { existed = 0; } + const bool needParam = (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0); + const std::string paramPath = mem_path(setup_param->user_id, setup_param->slot_id, "param"); + std::size_t paramSize = 0; + const bool haveParam = file_size_of(paramPath, ¶mSize); // First run: create a zero-filled blob and report existed size 0 so the title treats it as a new save. - if (!have || existed < setup_param->memory_size) { + if (!have || existed < setup_param->memory_size || (needParam && !haveParam)) { std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); + std::vector originalParam; + if (haveParam && !read_file_all(paramPath, originalParam)) { + return SAVE_DATA_ERROR_INTERNAL; + } std::vector data; if (have && !read_file_all(path, data)) { return SAVE_DATA_ERROR_INTERNAL; } - const std::vector originalData = data; - data.resize(setup_param->memory_size, 0); - if (!write_file_replace(path, data)) { - return SAVE_DATA_ERROR_INTERNAL; - } - if (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0) { + if (needParam) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); - if (!write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd)) { - if (!have) { - std::error_code removeError; - std::filesystem::remove(path, removeError); - } else { - write_file_replace(path, originalData); - } + if (!write_file_replace(paramPath, pd)) { return SAVE_DATA_ERROR_INTERNAL; } } + data.resize(setup_param->memory_size, 0); + if (!write_file_replace(path, data)) { + if (needParam) { + if (!haveParam) { + std::error_code removeError; + std::filesystem::remove(paramPath, removeError); + } else { + write_file_replace(paramPath, originalParam); + } + } + return SAVE_DATA_ERROR_INTERNAL; + } } if (result != nullptr) { std::memset(result, 0, sizeof(*result)); diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index c92d0f8f4..0c858c8f8 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -141,6 +141,15 @@ int main() { Require(Read(setupBinPath) == std::vector(128, 0)); const auto* setupBytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(setupBytes, setupBytes + sizeof(setupParam))); + std::vector seededBin(128); + for (std::size_t i = 0; i < seededBin.size(); ++i) { + seededBin[i] = static_cast(static_cast(i * 3 + 7)); + } + { + std::ofstream seededFile(setupBinPath, std::ios::binary); + seededFile.write(seededBin.data(), static_cast(seededBin.size())); + Require(static_cast(seededFile)); + } const auto growthParamTemp = setupParamPath.string() + ".tmp"; Require(std::filesystem::create_directory(growthParamTemp)); SaveDataParam previousParam = setupParam; @@ -149,15 +158,69 @@ int main() { setupResult.existed_memory_size = 999; Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 999); - Require(Read(setupBinPath) == std::vector(128, 0)); + Require(Read(setupBinPath) == seededBin); const auto* previousBytes = reinterpret_cast(&previousParam); Require(Read(setupParamPath) == std::vector(previousBytes, previousBytes + sizeof(previousParam))); Require(!std::filesystem::exists(growthParamTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 128); - Require(Read(setupBinPath) == std::vector(256, 0)); + std::vector expectedGrownBin = seededBin; + expectedGrownBin.resize(256, 0); + Require(Read(setupBinPath) == expectedGrownBin); const auto* updatedBytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); + const auto growthBinTemp = setupBinPath.string() + ".tmp"; + Require(std::filesystem::create_directory(growthBinTemp)); + SaveDataParam beforeBlobFailParam = setupParam; + setupParam.user_param = 300; + setup2.memory_size = 512; + setupResult.existed_memory_size = 888; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(setupResult.existed_memory_size == 888); + Require(Read(setupBinPath) == expectedGrownBin); + const auto* beforeBlobFailBytes = reinterpret_cast(&beforeBlobFailParam); + Require(Read(setupParamPath) == std::vector(beforeBlobFailBytes, beforeBlobFailBytes + sizeof(beforeBlobFailParam))); + Require(!std::filesystem::exists(growthBinTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 256); + std::vector expectedBlob512 = expectedGrownBin; + expectedBlob512.resize(512, 0); + Require(Read(setupBinPath) == expectedBlob512); + const auto* param300Bytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); + const auto orphanParamPath = std::filesystem::path("_sd_mem/u99/slot0.param"); + const auto orphanBinPath = std::filesystem::path("_sd_mem/u99/slot0.bin"); + Require(std::filesystem::create_directories(orphanBinPath.parent_path())); + const std::vector orphanBin(64, 'k'); + { + std::ofstream orphanFile(orphanBinPath, std::ios::binary); + orphanFile.write(orphanBin.data(), static_cast(orphanBin.size())); + Require(static_cast(orphanFile)); + } + SaveDataParam orphanParam{}; + orphanParam.user_param = 42; + SaveDataMemorySetup2 orphanSetup{}; + orphanSetup.user_id = 99; + orphanSetup.slot_id = 0; + orphanSetup.memory_size = 64; + orphanSetup.option = 1; + orphanSetup.init_param = &orphanParam; + SaveDataMemorySetupResult orphanResult{}; + orphanResult.existed_memory_size = 333; + Require(sceSaveDataSetupSaveDataMemory2(&orphanSetup, &orphanResult) == 0); + Require(orphanResult.existed_memory_size == 64); + Require(Read(orphanBinPath) == orphanBin); + const auto* orphanBytes = reinterpret_cast(&orphanParam); + Require(Read(orphanParamPath) == std::vector(orphanBytes, orphanBytes + sizeof(orphanParam))); + Require(sceSaveDataTerminate() == 0); + Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); + Require(sceSaveDataInitialize3(nullptr) == 0); + setupResult.existed_memory_size = 111; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 512); + Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); std::filesystem::current_path(previous); std::filesystem::remove_all(root); From 6a7cce415a1977e2827f7cbbaa50727ddb167c6b Mon Sep 17 00:00:00 2001 From: emirplus Date: Sat, 10 Oct 2026 21:09:00 +0300 Subject: [PATCH 003/318] fix(savedata): recover from metadata setup failure --- .../libs/prx/libSceSaveData.native/Export.cpp | 26 ++----- core/libs/tests/SaveDataMemoryGrowth.cpp | 4 +- core/libs/tests/SaveDataMemoryMetadata.cpp | 73 ++++++++++++++++++- 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 3cc62db23..86e94d97b 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -633,14 +633,6 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (!have || existed < setup_param->memory_size || (needParam && !haveParam)) { std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); - std::vector originalParam; - if (haveParam && !read_file_all(paramPath, originalParam)) { - return SAVE_DATA_ERROR_INTERNAL; - } - std::vector data; - if (have && !read_file_all(path, data)) { - return SAVE_DATA_ERROR_INTERNAL; - } if (needParam) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); @@ -648,17 +640,15 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat return SAVE_DATA_ERROR_INTERNAL; } } - data.resize(setup_param->memory_size, 0); - if (!write_file_replace(path, data)) { - if (needParam) { - if (!haveParam) { - std::error_code removeError; - std::filesystem::remove(paramPath, removeError); - } else { - write_file_replace(paramPath, originalParam); - } + if (!have || existed < setup_param->memory_size) { + std::vector data; + if (have && !read_file_all(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; + } + data.resize(setup_param->memory_size, 0); + if (!write_file_replace(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; } - return SAVE_DATA_ERROR_INTERNAL; } } if (result != nullptr) { diff --git a/core/libs/tests/SaveDataMemoryGrowth.cpp b/core/libs/tests/SaveDataMemoryGrowth.cpp index 924c4d4c6..83f70044b 100644 --- a/core/libs/tests/SaveDataMemoryGrowth.cpp +++ b/core/libs/tests/SaveDataMemoryGrowth.cpp @@ -82,10 +82,10 @@ int main(int argc, char**) { std::fprintf(stderr, "Read denial unavailable for this user\n"); exitCode = 77; } else { - Require(Read(path) == original); Require(status == static_cast(0x809F000Bu)); + Require(Read(path) == original); Require(result.existed_memory_size == 123); - Require(!std::filesystem::exists("_sd_mem/u7531/slot0.param")); + Require(std::filesystem::is_regular_file("_sd_mem/u7531/slot0.param")); Require(!std::filesystem::exists(path.string() + ".tmp")); } } else { diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index 0c858c8f8..8f40cccf5 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -171,23 +171,88 @@ int main() { Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); const auto growthBinTemp = setupBinPath.string() + ".tmp"; Require(std::filesystem::create_directory(growthBinTemp)); - SaveDataParam beforeBlobFailParam = setupParam; setupParam.user_param = 300; setup2.memory_size = 512; setupResult.existed_memory_size = 888; Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 888); Require(Read(setupBinPath) == expectedGrownBin); - const auto* beforeBlobFailBytes = reinterpret_cast(&beforeBlobFailParam); - Require(Read(setupParamPath) == std::vector(beforeBlobFailBytes, beforeBlobFailBytes + sizeof(beforeBlobFailParam))); + const auto* param300Bytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(!std::filesystem::exists(growthBinTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 256); std::vector expectedBlob512 = expectedGrownBin; expectedBlob512.resize(512, 0); Require(Read(setupBinPath) == expectedBlob512); - const auto* param300Bytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); + const auto freshFailBinPath = std::filesystem::path("_sd_mem/u55/slot0.bin"); + const auto freshFailParamPath = std::filesystem::path("_sd_mem/u55/slot0.param"); + Require(std::filesystem::create_directories(freshFailBinPath.parent_path())); + const auto freshFailBinTemp = freshFailBinPath.string() + ".tmp"; + Require(std::filesystem::create_directory(freshFailBinTemp)); + SaveDataParam freshFailParam{}; + freshFailParam.user_param = 55; + SaveDataMemorySetup2 freshFailSetup{}; + freshFailSetup.user_id = 55; + freshFailSetup.slot_id = 0; + freshFailSetup.memory_size = 64; + freshFailSetup.option = 1; + freshFailSetup.init_param = &freshFailParam; + SaveDataMemorySetupResult freshFailResult{}; + freshFailResult.existed_memory_size = 777; + Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == static_cast(0x809F000Bu)); + Require(freshFailResult.existed_memory_size == 777); + Require(!std::filesystem::exists(freshFailBinPath)); + const auto* freshFailBytes = reinterpret_cast(&freshFailParam); + Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); + Require(!std::filesystem::exists(freshFailBinTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == 0); + Require(freshFailResult.existed_memory_size == 0); + Require(Read(freshFailBinPath) == std::vector(64, 0)); + Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); + SaveDataParam noParam{}; + noParam.user_param = 5; + SaveDataMemorySetup2 noParamSetup{}; + SaveDataMemorySetupResult noParamResult{}; + noParamSetup.user_id = 56; + noParamSetup.slot_id = 0; + noParamSetup.memory_size = 16; + noParamSetup.option = 1; + noParamSetup.init_param = nullptr; + Require(sceSaveDataSetupSaveDataMemory2(&noParamSetup, &noParamResult) == 0); + Require(Read("_sd_mem/u56/slot0.bin") == std::vector(16, 0)); + Require(!std::filesystem::exists("_sd_mem/u56/slot0.param")); + noParamSetup.user_id = 57; + noParamSetup.option = 0; + noParamSetup.init_param = &noParam; + Require(sceSaveDataSetupSaveDataMemory2(&noParamSetup, &noParamResult) == 0); + Require(Read("_sd_mem/u57/slot0.bin") == std::vector(16, 0)); + Require(!std::filesystem::exists("_sd_mem/u57/slot0.param")); + SaveDataParam userA{}; + userA.user_param = 1; + SaveDataParam userB{}; + userB.user_param = 2; + SaveDataMemorySetup2 isoSetup{}; + SaveDataMemorySetupResult isoResult{}; + isoSetup.user_id = 60; + isoSetup.slot_id = 0; + isoSetup.memory_size = 8; + isoSetup.option = 1; + isoSetup.init_param = &userA; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + isoSetup.slot_id = 1; + isoSetup.init_param = &userB; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + const auto* userABytes = reinterpret_cast(&userA); + const auto* userBBytes = reinterpret_cast(&userB); + Require(Read("_sd_mem/u60/slot0.param") == std::vector(userABytes, userABytes + sizeof(userA))); + Require(Read("_sd_mem/u60/slot1.param") == std::vector(userBBytes, userBBytes + sizeof(userB))); + isoSetup.user_id = 61; + isoSetup.slot_id = 0; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + Require(Read("_sd_mem/u60/slot0.param") == std::vector(userABytes, userABytes + sizeof(userA))); + Require(Read("_sd_mem/u61/slot0.param") == std::vector(userBBytes, userBBytes + sizeof(userB))); const auto orphanParamPath = std::filesystem::path("_sd_mem/u99/slot0.param"); const auto orphanBinPath = std::filesystem::path("_sd_mem/u99/slot0.bin"); Require(std::filesystem::create_directories(orphanBinPath.parent_path())); From 06f84010bb82a4be475e213df7441c4833053b36 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:46:52 +0530 Subject: [PATCH 004/318] feat(libc): implement reallocf Two catalog titles stop on the absent libSceLibcInternal export reallocf (YMZO9ChZb0E) on main 0f863c10: - PPSA97510 PSXS5 2.2.0 - PPSA98510 SwanStationPS5 1.1.3 FAIL: unresolved ELF import YMZO9ChZb0E from libSceLibcInternal.prx reallocf follows FreeBSD 11 lib/libc/stdlib/reallocf.c (freebsd-src stable/11 5ed7eb0d97ba4436218e810f61bd059acba984c2) on top of the guest realloc: it reallocates through the application heap, and when realloc fails (the application allocator returns null or the default heap rejects the size) it frees the block through the same allocator, sets ENOMEM and returns null. Size 0 frees and returns null as realloc does here, so the block is not freed twice. Other heap failures throw, as for realloc. The NID matches tools/nid_names.py and aerolib. The application heap tests cover grow, shrink, a null pointer, size 0 and failure with both the replacement and the default allocator. Co-Authored-By: screamm Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/src/FileAndHeap.cpp | 13 +++++++++++++ core/libs/tests/ApplicationHeap.cpp | 26 ++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 40 insertions(+) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 20527e876..7234a1cef 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -284,6 +286,17 @@ void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); } +void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { + try { + return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); + } catch (const std::bad_alloc&) { + } catch (const std::length_error&) { + } + ApplicationHeapFree_nid_no_patch(ptr); + errno = 12; + return nullptr; +} + void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { return ApplicationHeapAlign_nid_no_patch(alignment, size); } diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 6cd531139..04adba57c 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -11,6 +11,7 @@ extern "C" { int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); +void* APS5_VABI reallocf_nid_postfix(void*, std::size_t); void* APS5_VABI _Znwm_nid_postfix(std::size_t); void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; @@ -148,6 +149,24 @@ int main(int argc, char** argv) { pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(nullptr, 32)); require(pointer != nullptr); ApplicationHeapFree_nid_no_patch(pointer); + pointer = static_cast(reallocf_nid_postfix(nullptr, 24)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 24); + std::memset(pointer, 0x3c, 24); + pointer = static_cast(reallocf_nid_postfix(pointer, 200)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 200); + for (unsigned i = 0; i < 24; ++i) require(pointer[i] == 0x3c); + pointer = static_cast(reallocf_nid_postfix(pointer, 9)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 9); + for (unsigned i = 0; i < 9; ++i) require(pointer[i] == 0x3c); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(pointer, 0) == nullptr && *__error_nid_postfix() == 0); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); + require(pointer != nullptr); + require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 12); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12); for (std::size_t alignment : {16, 64, 4096}) { auto* aligned = ApplicationHeapAlign_nid_no_patch(alignment, 37); require(reinterpret_cast(aligned) % alignment == 0); @@ -211,7 +230,14 @@ int main(int argc, char** argv) { require(frees == 6); _ZdlPvmSt11align_val_t_nid_postfix(nullptr, 37, 64); require(frees == 6); + require(reallocf_nid_postfix(storage.data(), 80) == storage.data() && lastSize == 80 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 16) == storage.data() && lastSize == 16 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 0) == nullptr && lastSize == 16 && frees == 7); fail = true; + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(storage.data(), 48) == nullptr && lastSize == 48 && frees == 8 && *__error_nid_postfix() == 12); + reject([] { ApplicationHeapReallocate_nid_no_patch(storage.data(), 48); }); + require(frees == 8); require(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); require(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); reject([] { _Znwm_nid_postfix(8); }); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..83fa934c2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -185,6 +185,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [malloc_usable_size](../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` +- [reallocf](../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (an exhausted guest arena, an unknown pointer) throw, as they do for `realloc` - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [sceImeGetPanelPositionAndForm](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills; it is never written, since no panel can be open. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title From 61a9f5dc14a3570af6253d49a1c1a1abad971c07 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:14:25 +0200 Subject: [PATCH 005/318] feat(agc): render 1_5_5_5 UNORM color targets CB_COLOR_INFO format 17 with UNORM and the standard component swap renders into VK_FORMAT_A1R5G5B5_UNORM_PACK16, whose attachment and blend support Vulkan requires. AMD keeps X in the low bits and Vulkan keeps R in the high bits, so the exports are mapped with X and Z exchanged and the stored bits follow the guest layout. PPSA06699 renders into this format. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 3 +++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 21 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 25 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index cf0d6b1ea..80aef8e63 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -456,6 +456,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (number == floating) return {VK_FORMAT_R32G32B32A32_SFLOAT, 16}; if (number == uint) return {VK_FORMAT_R32G32B32A32_UINT, 16}; return fail(); + case 17: + if (swap != 0 || number != unorm) return fail(); + return {VK_FORMAT_A1R5G5B5_UNORM_PACK16, 2, 0xc6u}; default: return fail(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..e2c8ad787 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -907,6 +907,26 @@ void ReversedComponentOrderTests() { } } +void Color1555TargetTests() { + auto queue = makeState(); + queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (17u << 2u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && state.colors[0].elementBytes == 2 && state.colors[0].componentMapping == 0xc6u, "a 1_5_5_5 target did not render through A1R5G5B5 with X and Z exchanged"); + Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u && state.blends[0].colorWriteMask == 0xfu, "a 1_5_5_5 target did not write all four channels through its export mapping"); + queue.context[0x8e] = 0x1u; + Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "a 1_5_5_5 target writing only X did not write the A1R5G5B5 channel in its low bits"); + queue.context[0x8e] = 0xfu; + for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; + queue.context[0x1e0] = 0x40000504u; + const auto blended = AgcDriver::Graphics::DecodeState(queue); + Require(blended.blends[0].blendEnable && blended.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA && blended.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, "alpha blending into a 1_5_5_5 target was not decoded"); + queue.context[0x1e0] = 0; + for (const auto& [number, swap] : {std::pair{1u, 0u}, std::pair{4u, 0u}, std::pair{0u, 1u}}) { + queue.context[0x31c] = (queue.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 17 number type " + std::to_string(number) + " component swap " + std::to_string(swap)); + } +} + void CompactedExportTests() { alignas(256) static std::array slotFourMemory{}; const auto slotFour = reinterpret_cast(slotFourMemory.data()); @@ -4303,6 +4323,7 @@ int main(int argc, char** argv) { ConservativeRasterizationTests(); DisabledColorTests(); OpaqueDestinationAlphaTests(); + Color1555TargetTests(); CompactedExportTests(); ReversedComponentOrderTests(); DualSourceBlendTests(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..f86055fbc 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -508,6 +508,7 @@ Throughout the project, every function at every stage either **does exactly what - [Comp-to-single DCC keys](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) is expanded only by a CB DCC decompress pass over uniform keys of a non-linear target: every texel takes the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. A fast-clear eliminate over such keys and mixed keys throw; textures and color targets that read them use their stored texels. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. +- [1_5_5_5 color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested; no draw into the target is executed in a test, and the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [COPY_DATA from the GPU clock](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [16-bit RGBA color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). The reverse orders (`COMP_SWAP` 2 and 3) keep throwing when blending, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. - [UINT16_ABGR color exports](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws From 17e03d3c0af4338bff1fb71328f80ec5e99f8a27 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:49:43 +0000 Subject: [PATCH 006/318] fix(libc): fail fgets with a size of 0 or less like FreeBSD The guest fgets handed the size straight to the host. With a size of 0 glibc and the Windows runtime return NULL without setting errno or the stream error indicator, and a negative size goes to the Windows invalid-parameter path. FreeBSD's fgets checks n <= 0 before reading: it sets __SERR on the stream, sets errno to EINVAL and returns NULL, leaving the buffer and the file position untouched. fgets_nid_postfix now does the same before calling the host. The FileStream flag it uses was only set for encoding errors so far and is renamed from SetEncodingError/encodingError to SetError/guestError; it already stays set until clearerr like __SERR. guest_standard_streams checks sizes 0, -1 and INT_MIN (NULL, errno 22, error indicator set and cleared by clearerr, buffer and position untouched) and that a following read still returns the line; it fails without the change. Fixes #3084 AI-assisted: yes Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/include/FileStream.hpp | 14 +++++------ core/libs/prx/libc/src/StandardStreams.cpp | 7 +++++- core/libs/tests/GuestStandardStreams.cpp | 27 ++++++++++++++++++++-- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 7a1b467be..9fff6280e 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -42,7 +42,7 @@ class FileStream { std::byte _reserved[256 - sizeof(GuestFilePrefix)]{}; std::FILE* _handle; bool _dynamic; - bool encodingError = false; + bool guestError = false; public: explicit FileStream(std::FILE* handle, bool dynamic = false) : _handle(handle), _dynamic(dynamic) { @@ -72,7 +72,7 @@ public: bool Reopen(const std::filesystem::path& filename, const char* mode) { auto* previous = GetHandle(); _guest = {}; - encodingError = false; + guestError = false; #ifdef _WIN32 std::wstring wideMode; for (const char character : std::string_view(mode)) wideMode.push_back(static_cast(character)); @@ -94,7 +94,7 @@ public: _guest.readRemaining = 0; _guest.writeRemaining = 0; _guest.flags = static_cast((_guest.flags & ~0x60) | - (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || encodingError ? 0x40 : 0)); + (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || guestError ? 0x40 : 0)); } void SyncUnlockedStatus() { @@ -108,17 +108,17 @@ public: const bool error = ::ferror_unlocked(GetHandle()) != 0; #endif _guest.flags = static_cast((_guest.flags & ~0x60) | - (endOfFile ? 0x20 : 0) | (error || encodingError ? 0x40 : 0)); + (endOfFile ? 0x20 : 0) | (error || guestError ? 0x40 : 0)); } - void SetEncodingError() { - encodingError = true; + void SetError() { + guestError = true; SyncStatus(); } void ClearError() { std::clearerr(GetHandle()); - encodingError = false; + guestError = false; SyncStatus(); } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 1d51cdb37..69e24de20 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -27,7 +27,7 @@ std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* strea if (value == -1) return -1; if (value < 0 || value > 255) { errno = 86; - stream->SetEncodingError(); + stream->SetError(); return -1; } const int result = std::ungetc(value, GetNativeStream(stream)); @@ -69,6 +69,11 @@ int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) { return result; } char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) { + if (size <= 0) { + stream->SetError(); + errno = 22; + return nullptr; + } auto* result = std::fgets(buffer, size, GetNativeStream(stream)); stream->SyncStatus(); return result; diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index cb428b927..c90016ce2 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -119,7 +120,7 @@ static void CheckUnlockedStreams() { Require(std::ungetc('Q', stream.GetHandle()) == 'Q'); Require(getc_unlocked_nid_postfix(&stream) == 'Q' && !(guest.flags & 0x20)); std::rewind(stream.GetHandle()); - stream.SetEncodingError(); + stream.SetError(); Require(putc_unlocked_nid_postfix('R', &stream) == 'R' && (guest.flags & 0x40)); clearerr_nid_postfix(&stream); Require(!(guest.flags & 0x40)); @@ -176,6 +177,27 @@ static void CheckUnlockedStreamLock(bool write) { stream.Close(); } +static void CheckFgetsSize() { + FileStream stream(std::tmpfile()); + Require(fwrite_nid_postfix("line\n", 1, 5, &stream) == 5); + rewind_nid_postfix(&stream); + for (const int size : {0, -1, std::numeric_limits::min()}) { + char buffer[8]; + std::memset(buffer, '!', sizeof(buffer)); + *__error_nid_postfix() = 0; + Require(fgets_nid_postfix(buffer, size, &stream) == nullptr); + Require(*__error_nid_postfix() == 22 && ferror_nid_postfix(&stream) != 0 && feof_nid_postfix(&stream) == 0); + Require(buffer[0] == '!' && ftello_nid_postfix(&stream) == 0); + Require(ferror_nid_postfix(&stream) != 0); + clearerr_nid_postfix(&stream); + Require(ferror_nid_postfix(&stream) == 0); + } + char line[8]{}; + Require(fgets_nid_postfix(line, sizeof(line), &stream) == line && std::strcmp(line, "line\n") == 0); + Require(ferror_nid_postfix(&stream) == 0); + stream.Close(); +} + static void CheckRewind() { FileStream stream(std::tmpfile()); Require(fwrite_nid_postfix("ab", 1, 2, &stream) == 2); @@ -186,7 +208,7 @@ static void CheckRewind() { rewind_nid_postfix(&stream); Require(fgetc_nid_postfix(&stream) == 'a' && fgetc_nid_postfix(&stream) == 'b' && fgetc_nid_postfix(&stream) == EOF); Require(feof_nid_postfix(&stream) != 0); - stream.SetEncodingError(); + stream.SetError(); Require(ferror_nid_postfix(&stream) != 0); rewind_nid_postfix(&stream); Require(feof_nid_postfix(&stream) == 0 && ferror_nid_postfix(&stream) == 0 && fgetc_nid_postfix(&stream) == 'a'); @@ -458,6 +480,7 @@ int main() { Require(fgetc_nid_postfix(&locked) == 'A' && fgetc_nid_postfix(&locked) == 'B'); locked.Close(); CheckRewind(); + CheckFgetsSize(); CheckUnlockedStreams(); CheckUnlockedStreamLock(false); CheckUnlockedStreamLock(true); From 48dcaace7868a55a831b196c46464163b816aa76 Mon Sep 17 00:00:00 2001 From: Ahir <191682168+ahir061@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:22:13 +0530 Subject: [PATCH 007/318] fix(shader): lock recompiler shared mutexes once when created winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with EINVAL, which std::shared_mutex ignores, so two threads can hold the lock at once. The source, binding plan and specialized module caches take their first locks from several threads, so their mutexes now take and release the lock in their constructor, as guest rwlocks do. The GuestWriteWatch mutex the technical debt entry also named is in the Linux-only watch, so it is not affected. --- core/shader/recompiler/Recompiler.cpp | 16 ++++++++++++---- docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 7e5883eeb..770bcef49 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -290,6 +290,14 @@ struct SourceEntry { namespace { +class PrimedSharedMutex : public std::shared_mutex { +public: + PrimedSharedMutex() { + lock(); + unlock(); + } +}; + struct ResourceProgram { explicit ResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr) : program(std::make_unique(PrepareResourceProgram(request, preparation))), plan(std::make_shared(ResourceMaterializer{}.ExtractPlan(*program))) {} @@ -318,7 +326,7 @@ bool FailureMemo() { } std::shared_ptr getSource(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr) { - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; // Entries whose code hashes alike share a bucket; the code comparison picks the right one. static std::unordered_map, std::vector>, SourceKeyHash> sources; struct SourceKeyStorage {}; @@ -587,7 +595,7 @@ struct PreparedBindingPlan { std::once_flag ready; std::exception_ptr failure; DescriptorBindingPlan bindings; - std::shared_mutex mutex; + PrimedSharedMutex mutex; std::map, std::shared_ptr> modules; }; @@ -618,7 +626,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } key.push_back(exports.size()); for (const auto mapping : exports) key.push_back(mapping); - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; static std::map, std::shared_ptr> plans; std::shared_ptr plan; { @@ -647,7 +655,7 @@ std::shared_ptr specializeModule(const CompiledShaderAr key.clear(); key.push_back(artifact.variantId); for (const auto& constant : constants) key.push_back((static_cast(constant.id) << 32u) | constant.value); - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; static std::map, std::shared_ptr> modules; std::shared_ptr entry; { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..cd6228162 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -7,7 +7,7 @@ - libgcc_s_seh-1.dll - libstdc++-6.dll - libwinpthread-1.dll -- winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) are locked once when created. The host `std::shared_mutex` objects in [Recompiler.cpp](../../core/shader/recompiler/Recompiler.cpp) and [GuestWriteWatch.cpp](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) are not, and can still race on their first contended use. +- winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) and the host `std::shared_mutex` objects in [Recompiler.cpp](../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. ### Silent stubs From 363d17985e2551dfbe3d3ec81ca3f4bb893b4fc7 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Fri, 9 Oct 2026 13:46:09 -0300 Subject: [PATCH 008/318] fix(relinker): lower legacy packed reciprocal register instructions --- .../x86/Amd64OnlySubstitutionTable.hpp | 2 + .../codegen/x86/ReciprocalOperands.hpp | 3 +- .../src/x86/Amd64OnlyInstructionMatcher.cpp | 8 +- .../codegen/src/x86/ReciprocalLowering.cpp | 8 +- .../codegen/src/x86/ReciprocalOperands.cpp | 16 +++- .../codegen/tests/Amd64OnlyConverterTests.cpp | 53 +++++++++--- .../tests/Amd64OnlyWindowsTests.cpp | 82 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 2 +- 8 files changed, 149 insertions(+), 25 deletions(-) diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index b7ee6f081..31118179e 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -49,6 +49,8 @@ inline constexpr Entry kMovntss = {"MOVNTSS", nullptr, 0}; inline constexpr Entry kMovntsd = {"MOVNTSD", nullptr, 0}; inline constexpr Entry kVrcpps = {"VRCPPS", nullptr, 0}; inline constexpr Entry kVrsqrtps = {"VRSQRTPS", nullptr, 0}; +inline constexpr Entry kRcpps = {"RCPPS", nullptr, 0}; +inline constexpr Entry kRsqrtps = {"RSQRTPS", nullptr, 0}; inline constexpr Entry kSha256rnds2 = {"SHA256RNDS2", nullptr, 0}; inline constexpr Entry kSha256msg1 = {"SHA256MSG1", nullptr, 0}; inline constexpr Entry kSha256msg2 = {"SHA256MSG2", nullptr, 0}; diff --git a/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp b/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp index 31102844e..ca3820623 100644 --- a/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp +++ b/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp @@ -16,9 +16,10 @@ struct ReciprocalOperands { ReciprocalOperation Operation; std::uint8_t Destination; std::uint8_t Source; + bool Vex; }; -[[nodiscard]] std::optional DecodeVexReciprocal(const std::uint8_t* data, std::size_t length); +[[nodiscard]] std::optional DecodeReciprocal(const std::uint8_t* data, std::size_t length); } diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 35b1e0cc8..5d33e8f82 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -41,6 +41,8 @@ const Entry& _sse4aEntry(const Sse4aOperands& operands) { } const Entry& _reciprocalEntry(const ReciprocalOperands& operands) { + if (!operands.Vex) + return operands.Operation == ReciprocalOperation::ReciprocalSquareRoot ? kRsqrtps : kRcpps; return operands.Operation == ReciprocalOperation::ReciprocalSquareRoot ? kVrsqrtps : kVrcpps; } @@ -239,7 +241,7 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( if (name == nullptr) name = kClzero.Name; _clzeroLowering.EmitOutOfLine(body, operands); - } else if (const auto reciprocal = DecodeVexReciprocal(instr.Data, instr.Length)) { + } else if (const auto reciprocal = DecodeReciprocal(instr.Data, instr.Length)) { if (name == nullptr) name = _reciprocalEntry(*reciprocal).Name; _reciprocalLowering.EmitOutOfLine(body, *reciprocal); @@ -254,7 +256,7 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( } _moveTrailing(body, trailing); const DecodedInstruction first{instructions.front().data(), instructions.front().size()}; - const bool optional = DecodeVexReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && first.IsRdpid()); + const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && first.IsRdpid()); return _trampoline(name, instructions.front().size(), body.Finish(), optional); } @@ -289,7 +291,7 @@ std::optional Amd64OnlyInstructionMatcher::Match( if (instr.IsMwaitx()) return _validWait(instr) ? _inPlace(kMwaitx, length, std::vector(kPause.Bytes, kPause.Bytes + kPause.Size)) : _unsupported(kMwaitx, length); - if (const auto reciprocal = DecodeVexReciprocal(data, length)) { + if (const auto reciprocal = DecodeReciprocal(data, length)) { return _trampoline(_reciprocalEntry(*reciprocal).Name, length, _outOfLine(length, trailing, [&](StubBodyBuilder& body) { _reciprocalLowering.EmitOutOfLine(body, *reciprocal); }), true); } diff --git a/core/relinker/codegen/src/x86/ReciprocalLowering.cpp b/core/relinker/codegen/src/x86/ReciprocalLowering.cpp index 8f6d794b4..d01588ae8 100644 --- a/core/relinker/codegen/src/x86/ReciprocalLowering.cpp +++ b/core/relinker/codegen/src/x86/ReciprocalLowering.cpp @@ -46,11 +46,15 @@ void ReciprocalLowering::EmitOutOfLine(StubBodyBuilder& body, const ReciprocalOp body.SsePlain({0x0F, kSqrtps}, scratch, operands.Source); body.RipOperand({0x0F, kMovaps}, dst, _ones()); body.SsePlain({0x0F, kDivps}, dst, scratch); - _vexMovaps(body, dst, dst); + if (operands.Vex) + _vexMovaps(body, dst, dst); } else { body.RipOperand({0x0F, kMovaps}, scratch, _ones()); body.SsePlain({0x0F, kDivps}, scratch, operands.Source); - _vexMovaps(body, dst, scratch); + if (operands.Vex) + _vexMovaps(body, dst, scratch); + else + body.SsePlain({0x0F, kMovaps}, dst, scratch); } body.Restore(scratch); } diff --git a/core/relinker/codegen/src/x86/ReciprocalOperands.cpp b/core/relinker/codegen/src/x86/ReciprocalOperands.cpp index c9e203768..a985003b5 100644 --- a/core/relinker/codegen/src/x86/ReciprocalOperands.cpp +++ b/core/relinker/codegen/src/x86/ReciprocalOperands.cpp @@ -12,14 +12,15 @@ constexpr std::uint8_t kOpcodeRcpps = 0x53; } -std::optional DecodeVexReciprocal(const std::uint8_t* data, const std::size_t length) { +std::optional DecodeReciprocal(const std::uint8_t* data, const std::size_t length) { using namespace X64OpcodeConstants; - if (length < 4) + if (length < 3) return std::nullopt; bool registerExtension = false; bool rmExtension = false; std::uint8_t payload = 0; std::size_t opcodeOffset = 0; + bool vex = true; if (data[0] == OneByteVex2 && length == 4) { registerExtension = (data[1] & 0x80) == 0; payload = data[1]; @@ -31,13 +32,21 @@ std::optional DecodeVexReciprocal(const std::uint8_t* data, rmExtension = (data[1] & 0x20) == 0; payload = data[2]; opcodeOffset = 3; + } else if (data[0] == TwoByteOpcodeEscape && length == 3) { + opcodeOffset = 1; + vex = false; + } else if (data[0] >= RexMin && data[0] <= RexMax && length == 4 && data[1] == TwoByteOpcodeEscape) { + registerExtension = (data[0] & 0x04) != 0; + rmExtension = (data[0] & 0x01) != 0; + opcodeOffset = 2; + vex = false; } else { return std::nullopt; } const auto vvvv = static_cast((payload >> 3) & 0x0F); const bool wide = (payload & 0x04) != 0; const auto pp = static_cast(payload & 0x03); - if (vvvv != kVexUnusedRegister || wide || pp != 0) + if (vex && (vvvv != kVexUnusedRegister || wide || pp != 0)) return std::nullopt; const auto opcode = data[opcodeOffset]; if (opcode != kOpcodeRsqrtps && opcode != kOpcodeRcpps) @@ -49,6 +58,7 @@ std::optional DecodeVexReciprocal(const std::uint8_t* data, operands.Operation = opcode == kOpcodeRsqrtps ? ReciprocalOperation::ReciprocalSquareRoot : ReciprocalOperation::Reciprocal; operands.Destination = static_cast(((modrm >> ModRmRegShift) & ModRmRegMask) | (registerExtension ? 8 : 0)); operands.Source = static_cast((modrm & ModRmRmMask) | (rmExtension ? 8 : 0)); + operands.Vex = vex; return operands; } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index dba57fd9f..719788893 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -700,17 +700,23 @@ void converterRosetta() { } void reciprocalOperands() { - const auto vex2 = Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x52, 0xD5}.data(), 4); + const auto vex2 = Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x52, 0xD5}.data(), 4); require(vex2 && vex2->Operation == Codegen::ReciprocalOperation::ReciprocalSquareRoot && vex2->Destination == 2 && vex2->Source == 5, "VEX2 VRSQRTPS was not decoded"); - const auto vex2High = Codegen::DecodeVexReciprocal(Bytes{0xC5, 0x78, 0x53, 0xC1}.data(), 4); + const auto vex2High = Codegen::DecodeReciprocal(Bytes{0xC5, 0x78, 0x53, 0xC1}.data(), 4); require(vex2High && vex2High->Operation == Codegen::ReciprocalOperation::Reciprocal && vex2High->Destination == 8 && vex2High->Source == 1, "VEX2 VRCPPS with a high destination was not decoded"); - const auto vex3 = Codegen::DecodeVexReciprocal(Bytes{0xC4, 0x41, 0x78, 0x52, 0xC9}.data(), 5); + const auto vex3 = Codegen::DecodeReciprocal(Bytes{0xC4, 0x41, 0x78, 0x52, 0xC9}.data(), 5); require(vex3 && vex3->Destination == 9 && vex3->Source == 9, "VEX3 VRSQRTPS with high registers was not decoded"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xFC, 0x52, 0xD5}.data(), 4), "256-bit VRSQRTPS must stay native"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xFA, 0x52, 0xD5}.data(), 4), "VRSQRTSS must stay native"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x52, 0x10}.data(), 4), "Memory form must stay native"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x51, 0xD5}.data(), 4), "VSQRTPS is not a reciprocal"); - require(!Codegen::DecodeVexReciprocal(Bytes{0x0F, 0x52, 0xD5}.data(), 3), "Legacy RSQRTPS must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xFC, 0x52, 0xD5}.data(), 4), "256-bit VRSQRTPS must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xFA, 0x52, 0xD5}.data(), 4), "VRSQRTSS must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x52, 0x10}.data(), 4), "Memory form must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x51, 0xD5}.data(), 4), "VSQRTPS is not a reciprocal"); + require(vex2->Vex && vex2High->Vex && vex3->Vex, "VEX reciprocals must clear the upper vector bits"); + const auto legacy = Codegen::DecodeReciprocal(Bytes{0x0F, 0x52, 0xD5}.data(), 3); + require(legacy && !legacy->Vex && legacy->Operation == Codegen::ReciprocalOperation::ReciprocalSquareRoot && legacy->Destination == 2 && legacy->Source == 5, "Legacy RSQRTPS was not decoded"); + const auto legacyHigh = Codegen::DecodeReciprocal(Bytes{0x45, 0x0F, 0x53, 0xC9}.data(), 4); + require(legacyHigh && !legacyHigh->Vex && legacyHigh->Operation == Codegen::ReciprocalOperation::Reciprocal && legacyHigh->Destination == 9 && legacyHigh->Source == 9, "Legacy RCPPS with high registers was not decoded"); + for (const Bytes& unsupported : {Bytes{0xF3, 0x0F, 0x52, 0xD5}, Bytes{0xF3, 0x0F, 0x53, 0xD5}, Bytes{0x0F, 0x52, 0x10}, Bytes{0x0F, 0x53, 0x10}, Bytes{0x0F, 0x51, 0xD5}, Bytes{0x0F, 0x52}, Bytes{0x45, 0x0F, 0x53}}) + require(!Codegen::DecodeReciprocal(unsupported.data(), unsupported.size()), "An unsupported legacy reciprocal encoding was accepted"); } void converterReciprocal() { @@ -744,6 +750,27 @@ void converterReciprocal() { require(moved.Trampolines.size() == 1 && moved.KeptCount == 0 && moved.Bytes == ripRelative && moved.Trampolines[0].Length == 10, "VRSQRTPS did not absorb the following RIP-relative load"); const auto& movedSite = moved.Trampolines[0]; require(movedSite.Relocations.size() == 1 && movedSite.Relocations[0].DisplacementOffset == movedSite.ReturnBranchOffset - 4 && movedSite.Relocations[0].InstructionEnd == movedSite.ReturnBranchOffset && movedSite.Relocations[0].SiteTarget == 10 + 0x10, "Absorbed RIP-relative load has the wrong relocation"); + for (const std::uint8_t opcode : {std::uint8_t{0x52}, std::uint8_t{0x53}}) { + const std::string name = opcode == 0x52 ? "RSQRTPS" : "RCPPS"; + for (const Bytes& instruction : {Bytes{0x0F, opcode, 0xD5}, Bytes{0x45, 0x0F, opcode, 0xC9}}) { + auto legacyFile = file; + Bytes legacyText = instruction; + legacyText.insert(legacyText.end(), {0x90, 0x90, 0xC3}); + std::copy(legacyText.begin(), legacyText.end(), legacyFile.begin() + 0x200); + const auto legacy = converter->Convert(legacyFile, {segmentHeader(legacyText.size())}); + require(legacy.Trampolines.size() == 1 && legacy.KeptCount == 0 && legacy.Reports[0].InstructionName == name && legacy.Trampolines[0].Length == 5, "Legacy reciprocal did not absorb enough instructions for a jump"); + legacyText = instruction; + legacyText.push_back(0xC3); + std::copy(legacyText.begin(), legacyText.end(), legacyFile.begin() + 0x200); + const auto kept = converter->Convert(legacyFile, {segmentHeader(legacyText.size())}); + require(kept.Trampolines.empty() && kept.KeptCount == 1 && kept.Bytes == legacyFile && kept.Reports[0].InstructionName == name, "Legacy reciprocal before a return must stay native"); + } + } + auto sequenceFile = file; + const Bytes sequenceText = {0x0F, 0x52, 0xD5, 0xC5, 0xF8, 0x53, 0xC1, 0xC3}; + std::copy(sequenceText.begin(), sequenceText.end(), sequenceFile.begin() + 0x200); + const auto sequence = converter->Convert(sequenceFile, {segmentHeader(sequenceText.size())}); + require(sequence.Trampolines.size() == 1 && sequence.KeptCount == 0 && sequence.Trampolines[0].Length == 7 && sequence.Reports[0].InstructionName == "RSQRTPS", "Mixed legacy and VEX reciprocals were not lowered together"); } void converterStrayRex() { @@ -1037,10 +1064,10 @@ void reciprocalExecution() { for (const auto& lanes : inputs) { const std::uint64_t source[2] = {packLanes(lanes, 0), packLanes(lanes, 2)}; const std::uint64_t destination[2] = {0x1111111111111111ull, 0x2222222222222222ull}; - for (const bool squareRoot : {true, false}) { + for (const bool squareRoot : {true, false}) for (const bool vex : {false, true}) { const std::uint8_t opcode = squareRoot ? 0x52 : 0x53; - const Bytes distinct = {0xC5, 0xF8, opcode, 0xD5}; - const Bytes same = {0xC5, 0xF8, opcode, 0xD2}; + const Bytes distinct = vex ? Bytes{0xC5, 0xF8, opcode, 0xD5} : Bytes{0x0F, opcode, 0xD5}; + const Bytes same = vex ? Bytes{0xC5, 0xF8, opcode, 0xD2} : Bytes{0x0F, opcode, 0xD2}; const auto distinctOut = runRegisterFormStub(distinct, destination, source); const auto sameOut = runRegisterFormStub(same, source, source); float distinctLanes[4]; @@ -1049,8 +1076,8 @@ void reciprocalExecution() { std::memcpy(sameLanes, sameOut.data(), sizeof(sameLanes)); for (std::size_t lane = 0; lane < 4; ++lane) { const float expected = squareRoot ? 1.0f / std::sqrt(lanes[lane]) : 1.0f / lanes[lane]; - require(sameLane(expected, distinctLanes[lane]), "VRSQRTPS/VRCPPS stub is not correctly rounded"); - require(sameLane(expected, sameLanes[lane]), "VRSQRTPS/VRCPPS stub with equal operands is not correctly rounded"); + require(sameLane(expected, distinctLanes[lane]), "Packed reciprocal stub is not correctly rounded"); + require(sameLane(expected, sameLanes[lane]), "Packed reciprocal stub with equal operands is not correctly rounded"); } } } diff --git a/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp b/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp index f88d3d93f..c726b2958 100644 --- a/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp +++ b/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ struct alignas(16) State { std::uint64_t RspBefore; std::uint64_t RspAfter; std::uint8_t RedZone[128]; + std::uint8_t Ymm[16][32]; }; constexpr std::uint64_t kCanary = 0xA5C3E17B9D24F608ull; @@ -94,6 +96,11 @@ void movdquXmmRsp(Bytes& code, const std::uint8_t reg, const bool store, const s emitU32(code, offset); } +void vmovdquYmmRbx(Bytes& code, const std::uint8_t reg, const bool store) { + emit(code, {0xC4, static_cast(reg >= 8 ? 0x61 : 0xE1), 0x7E, static_cast(store ? 0x7F : 0x6F), static_cast(0x83 | ((reg & 7) << 3))}); + emitU32(code, static_cast(offsetof(State, Ymm)) + reg * 32); +} + class Harness { public: Harness() : _memory(VirtualAlloc(nullptr, kSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) { @@ -104,7 +111,7 @@ public: VirtualFree(_memory, 0, MEM_RELEASE); } - void Run(const Bytes& body, const std::size_t returnBranchOffset, State& state) const { + void Run(const Bytes& body, const std::size_t returnBranchOffset, State& state, const bool wide = false) const { Bytes code; emit(code, {0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x81, 0xEC, 0xA8, 0x00, 0x00, 0x00}); for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, true, (reg - 6) * 16); @@ -115,7 +122,12 @@ public: for (int slot = 0; slot < 16; ++slot) emit(code, {0x48, 0x89, 0x44, 0x24, static_cast(-128 + slot * 8)}); emit(code, {0x48, 0x89, 0xA3}); emitU32(code, kRspBeforeOffset); - for (std::uint8_t reg = 0; reg < 16; ++reg) movdquXmmRbx(code, reg, false); + for (std::uint8_t reg = 0; reg < 16; ++reg) { + if (wide) + vmovdquYmmRbx(code, reg, false); + else + movdquXmmRbx(code, reg, false); + } code.push_back(0xE9); const auto jumpOffset = code.size(); emitU32(code, 0); @@ -129,6 +141,8 @@ public: emit(code, {0x9C, 0x8F, 0x83}); emitU32(code, kFlagsOutOffset); for (std::uint8_t reg = 0; reg < 16; ++reg) movdquXmmRbx(code, reg, true); + if (wide) + for (std::uint8_t reg = 0; reg < 16; ++reg) vmovdquYmmRbx(code, reg, true); for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, false, (reg - 6) * 16); emit(code, {0x48, 0x81, 0xC4, 0xA8, 0x00, 0x00, 0x00, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5F, 0x5E, 0x5D, 0x5B, 0xC3}); code.resize(Io::AlignUp(code.size(), std::size_t{16}), 0xCC); @@ -219,6 +233,69 @@ void requireEnvironment(const State& input, const State& state, const std::uint1 } } +void reciprocalExecution() { + const Harness harness; + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const bool wide = __builtin_cpu_supports("avx"); + const float inputs[][4] = { + {1.0f, 4.0f, 0.25f, 2.0f}, + {0.0f, -0.0f, INFINITY, -1.0f}, + {1.00000012f, 0.99999994f, 3.0e-38f, 1.0e30f}, + {NAN, -INFINITY, -4.0f, 16.0f}}; + for (const bool vex : {false, true}) { + if (vex && !wide) + continue; + for (const std::uint8_t opcode : {std::uint8_t{0x52}, std::uint8_t{0x53}}) { + for (const std::uint8_t destination : {std::uint8_t{0}, std::uint8_t{2}, std::uint8_t{9}, std::uint8_t{15}}) { + for (const std::uint8_t source : {destination, std::uint8_t{0}, std::uint8_t{5}, std::uint8_t{14}}) { + Bytes site; + if (vex) { + emit(site, {0xC4, static_cast((destination >= 8 ? 0 : 0x80) | 0x40 | (source >= 8 ? 0 : 0x20) | 1), 0x78}); + } else { + const auto rex = static_cast(0x40 | (destination >= 8 ? 4 : 0) | (source >= 8 ? 1 : 0)); + if (rex != 0x40) + site.push_back(rex); + site.push_back(0x0F); + } + emit(site, {opcode, static_cast(0xC0 | ((destination & 7) << 3) | (source & 7))}); + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "Packed reciprocal was not lowered"); + for (const auto& lanes : inputs) { + State input{}; + for (std::size_t reg = 0; reg < 16; ++reg) { + for (std::size_t byte = 0; byte < 16; ++byte) { + input.Xmm[reg][byte] = static_cast(reg * 16 + byte); + input.Ymm[reg][byte + 16] = static_cast(reg * 16 + byte + 1); + } + } + std::memcpy(input.Xmm[source], lanes, sizeof(lanes)); + for (std::size_t reg = 0; reg < 16; ++reg) + std::memcpy(input.Ymm[reg], input.Xmm[reg], 16); + input.FlagsIn = 0xAC7; + State state = input; + harness.Run(match->StubBody, match->ReturnBranchOffset, state, wide); + ++g_executions; + float actual[4]; + std::memcpy(actual, state.Xmm[destination], sizeof(actual)); + for (std::size_t lane = 0; lane < 4; ++lane) { + const float expected = opcode == 0x52 ? 1.0f / std::sqrt(lanes[lane]) : 1.0f / lanes[lane]; + require(std::isnan(expected) ? std::isnan(actual[lane]) : std::memcmp(&expected, &actual[lane], sizeof(float)) == 0, "Packed reciprocal result differs from division and square root"); + } + requireEnvironment(input, state, static_cast(1u << destination), "packed reciprocal"); + if (wide) { + const std::uint8_t zero[16] = {}; + for (std::size_t reg = 0; reg < 16; ++reg) { + const auto* expected = vex && reg == destination ? zero : input.Ymm[reg] + 16; + require(std::memcmp(state.Ymm[reg] + 16, expected, 16) == 0, "Packed reciprocal changed the wrong upper YMM bits"); + } + } + } + } + } + } + } +} + void executeCase(const Harness& harness, const Codegen::IAmd64OnlyInstructionMatcher& matcher, const Case& item, std::mt19937_64& random, const int images) { const auto site = encode(item); const auto match = matcher.Match(site.data(), site.size()); @@ -643,6 +720,7 @@ void peBuilder() { int main() { try { cpuExecution(); + reciprocalExecution(); peBuilder(); std::cout << "AMD64-only Windows tests passed (" << g_executions << " lowered sequences executed)\n"; } catch (const std::exception& error) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..5f243bc03 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -513,7 +513,7 @@ Throughout the project, every function at every stage either **does exactly what - [UINT16_ABGR color exports](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE but reports none of them through CPUID, so code that checks CPUID takes its baseline path. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests; that the converted code runs under Rosetta was checked on an M5 Max only, outside this repository's CI. -- `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). +- `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates the 101-key (US) arrangement and the alphanumeric layer of the 106-key (Japanese) one, which follows the JIS layout of the HID usages: the Yen (0x89) and Ro (0x87) keys return `\` (0x5C) unshifted, as JIS X 0201 places the yen sign there; whether the console returns U+00A5 instead is unverified. With the Kana LED (bit 4, the HID position) set the 106-key arrangement throws, as the kana the console returns are unknown. Ctrl and Alt do not change the character. - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [sceImeKeyboardGetInfo](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. From 09894076d4e57c4780623415c0f833eb9d6d4e93 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:01:34 +0200 Subject: [PATCH 009/318] fix(agc): own resolved dispatch state through thread exit The resolved-dispatch map was a nontrivial thread_local object. MinGW can run its destructor after emutls frees the backing storage, causing an access violation when a queue worker exits. Use the existing ThreadOwned helper to destroy the heap-owned map through its pthread key while keeping a trivial TLS pointer. Reject FATAL output in both draw-cache test modes so a background shutdown failure cannot pass only because the process returns zero. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../Execution/src/Driver/Dispatch/DispatchAhead.cpp | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..5c98010b2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1070,6 +1070,7 @@ if(BUILD_TESTING) endif() add_test(NAME agc_driver_draw_cache_reuse_off COMMAND agc_driver_draw_cache_reuse_tests) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_GPL=1" SKIP_RETURN_CODE 77) + set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") if(WIN32) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 60d370b08..1ca05db5c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -10,8 +11,8 @@ namespace AgcDriver::DriverDetail { std::map& Driver::resolvedAhead() { - static thread_local std::map resolved; - return resolved; + static thread_local std::map* resolved = nullptr; + return ShaderRecompiler::ThreadOwned(resolved); } Driver::GroupCaptureStats& Driver::groupCaptureStats() { From 632338cd65e0aeb44f96f1e69a998d4d0c898771 Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:07:19 -0400 Subject: [PATCH 010/318] fix(libc): fmod leaves errno unchanged fmod set errno (33) for 1.0, 0.0. FreeBSD msun never sets errno and reports errors through floating-point exceptions only, so errno is now saved and restored around the host call. Co-Authored-By: Claude Haiku 5.5 --- core/libs/prx/libc/src/Math.cpp | 7 ++++++- core/libs/tests/GuestMath.cpp | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9d6a8aebd..70757c8b4 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -127,7 +127,12 @@ float APS5_VABI logf_nid_postfix(float x) { return std::log(x); } float APS5_VABI log2f_nid_postfix(float x) { return std::log2(x); } double APS5_VABI log10_nid_postfix(double x) { return std::log10(x); } float APS5_VABI ldexpf_nid_postfix(float x, int exp) { return std::ldexp(x, exp); } -double APS5_VABI fmod_nid_postfix(double x, double y) { return std::fmod(x, y); } +double APS5_VABI fmod_nid_postfix(double x, double y) { + int saved_errno = errno; + double result = std::fmod(x, y); + errno = saved_errno; + return result; +} float APS5_VABI roundf_nid_postfix(float x) { return std::round(x); } double APS5_VABI round_nid_postfix(double x) { return std::round(x); } float APS5_VABI cbrtf_nid_postfix(float x) { return std::cbrt(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6d56671b5..b136655ec 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -38,6 +38,7 @@ extern LibcFloatConstant _FNan_nid_postfix; short APS5_VABI _FDtest_nid_postfix(const float*); int APS5_VABI __fpclassifyf_nid_postfix(float); float APS5_VABI fmodf_nid_postfix(float, float); +double APS5_VABI fmod_nid_postfix(double, double); float APS5_VABI asinf_nid_postfix(float); float APS5_VABI acosf_nid_postfix(float); float APS5_VABI atan2f_nid_postfix(float, float); @@ -150,6 +151,13 @@ static void CheckLongDouble() { Require(std::isnan(frexpl_nid_postfix(NAN, &exponent)) && exponent == 77); } +static void CheckFmodErrno() { + errno = 0; + (void)fmod_nid_postfix(1.0, 0.0); + Require(errno == 0); + Require(fmod_nid_postfix(1e300, 3.0) == std::fmod(1e300, 3.0)); +} + static void CheckIntegerConversions() { for (const long long numerator : {4294967301LL, -4294967301LL}) { for (const long long denominator : {3LL, -3LL}) { @@ -493,4 +501,5 @@ int main() { Require(abs_nid_postfix(std::numeric_limits::min()) == std::numeric_limits::min()); CheckHyperbolic(); CheckHypotSpecialValue(); + CheckFmodErrno(); } From 66165443c0e6de753f247d34c0a6e6b46baf7745 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:20:47 +0200 Subject: [PATCH 011/318] feat(libSceCesCs): add sceCesRefersUcsProfileCp1251 and sceCesMbcsUcsContextInit PPSA15246 (Teardown) imports both from the module-named libSceCesCs-module and stops at the loader without them. Cp1251 joins Cp1252 in sceCesSbcToUtf8 and sceCesUtf8ToSbc; the context initializer succeeds without storing anything because its layout is unknown (listed in TechnicalDebt.md). Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceCesCs-module/CMakeLists.txt | 3 ++ core/libs/prx/libSceCesCs/Export.cpp | 53 ++++++++++++++++--- core/libs/tests/GuestCes.cpp | 26 +++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceCesCs-module/CMakeLists.txt diff --git a/core/libs/prx/libSceCesCs-module/CMakeLists.txt b/core/libs/prx/libSceCesCs-module/CMakeLists.txt new file mode 100644 index 000000000..4f216c2f6 --- /dev/null +++ b/core/libs/prx/libSceCesCs-module/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(libSceCesCs-module SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/../libSceCesCs/Export.cpp) +target_include_directories(libSceCesCs-module PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceCesCs-module) diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index a2649662d..489bc94b3 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -13,18 +13,45 @@ constexpr int CES_ERROR_UNASSIGNED_CODE = static_cast(0x805C0020); constexpr int CES_ERROR_INVALID_DST_BUFFER = static_cast(0x805C0030); constexpr int CES_ERROR_DST_BUFFER_END = static_cast(0x805C0031); -constexpr uint8_t CP1252_PROFILE = 0; +constexpr uint8_t PROFILES[2] = {0, 1}; +constexpr const uint8_t* CP1252_PROFILE = &PROFILES[0]; +constexpr const uint8_t* CP1251_PROFILE = &PROFILES[1]; constexpr uint16_t CP1252_HIGH[32] = { 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178, }; -uint32_t cp1252ToUnicode(uint8_t sbc) { +constexpr uint16_t CP1251_HIGH[128] = { + 0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, + 0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0098, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, + 0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, + 0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457, + 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, + 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, + 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, + 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, +}; + +uint32_t sbcToUnicode(const uint8_t* profile, uint8_t sbc) { + if (profile == CP1251_PROFILE) return sbc >= 0x80 ? CP1251_HIGH[sbc - 0x80] : sbc; return sbc >= 0x80 && sbc < 0xA0 ? CP1252_HIGH[sbc - 0x80] : sbc; } -bool unicodeToCp1252(uint32_t code, uint8_t& sbc) { +bool unicodeToSbc(const uint8_t* profile, uint32_t code, uint8_t& sbc) { + if (profile == CP1251_PROFILE) { + if (code < 0x80) { + sbc = static_cast(code); + return true; + } + for (uint32_t i = 0; i < 128; ++i) { + if (CP1251_HIGH[i] == code) { + sbc = static_cast(0x80 + i); + return true; + } + } + return false; + } if (code < 0x80 || (code >= 0xA0 && code <= 0xFF)) { sbc = static_cast(code); return true; @@ -81,15 +108,25 @@ uint32_t encodeUtf8(uint32_t code, uint8_t* utf8) { extern "C" { const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void) { - return &CP1252_PROFILE; + return CP1252_PROFILE; +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void) { + return CP1251_PROFILE; +} + +int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile) { + (void)profile; + if (!context) return CES_ERROR_INVALID_PARAMETER; + return 0; } int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len) { - if (profile != &CP1252_PROFILE) return CES_ERROR_INVALID_PARAMETER; + if (profile != CP1252_PROFILE && profile != CP1251_PROFILE) return CES_ERROR_INVALID_PARAMETER; if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; uint8_t encoded[3]; - const uint32_t length = encodeUtf8(cp1252ToUnicode(sbc), encoded); + const uint32_t length = encodeUtf8(sbcToUnicode(profile, sbc), encoded); if (utf8max < length) return CES_ERROR_DST_BUFFER_END; for (uint32_t i = 0; i < length; ++i) utf8[i] = encoded[i]; if (utf8_len) *utf8_len = length; @@ -97,7 +134,7 @@ int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8 } int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len, const uint8_t* profile, uint8_t* sbc) { - if (profile != &CP1252_PROFILE) return CES_ERROR_INVALID_PARAMETER; + if (profile != CP1252_PROFILE && profile != CP1251_PROFILE) return CES_ERROR_INVALID_PARAMETER; if (!utf8) return CES_ERROR_INVALID_SRC_BUFFER; if (utf8max == 0) return CES_ERROR_SRC_BUFFER_END; if (!sbc) return CES_ERROR_INVALID_DST_BUFFER; @@ -105,7 +142,7 @@ int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* u uint32_t length = 0; uint32_t code = 0; if (const int result = decodeUtf8(utf8, utf8max, length, code); result != 0) return result; - if (!unicodeToCp1252(code, *sbc)) return CES_ERROR_UNASSIGNED_CODE; + if (!unicodeToSbc(profile, code, *sbc)) return CES_ERROR_UNASSIGNED_CODE; if (utf8_len) *utf8_len = length; return 0; } diff --git a/core/libs/tests/GuestCes.cpp b/core/libs/tests/GuestCes.cpp index 985713bcb..55bd86742 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -5,6 +5,8 @@ extern "C" { const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void); +int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); int APS5_VABI sceCesSbcToUtf8(const std::uint8_t*, std::uint8_t, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesUtf8ToSbc(const std::uint8_t*, std::uint32_t, std::uint32_t*, const std::uint8_t*, std::uint8_t*); } @@ -80,4 +82,28 @@ int main() { Require(Utf8ToSbc(profile, "\xED\xA0\x80", 3) == invalidEncode); Require(Utf8ToSbc(profile, "\xE3\x81\x82", 3) == unassignedCode); Require(Utf8ToSbc(profile, "\xF0\x9F\x98\x80", 4) == unassignedCode); + + const std::uint8_t* cyrillic = sceCesRefersUcsProfileCp1251(); + Require(cyrillic != nullptr && cyrillic == sceCesRefersUcsProfileCp1251() && cyrillic != profile); + RequireSbcToUtf8(cyrillic, 'A', "A"); + RequireSbcToUtf8(cyrillic, 0x88, "\xE2\x82\xAC"); + RequireSbcToUtf8(cyrillic, 0xC0, "\xD0\x90"); + RequireSbcToUtf8(cyrillic, 0xFF, "\xD1\x8F"); + RequireSbcToUtf8(cyrillic, 0xA8, "\xD0\x81"); + RequireSbcToUtf8(cyrillic, 0xB9, "\xE2\x84\x96"); + RequireUtf8ToSbc(cyrillic, "\xD0\x9F", 0xCF); + RequireUtf8ToSbc(cyrillic, "\xD1\x91", 0xB8); + for (int sbc = 1; sbc < 256; ++sbc) { + std::uint8_t cp[4] = {}; + std::uint32_t cpLength = 0; + std::uint8_t back = 0; + Require(sceCesSbcToUtf8(cyrillic, static_cast(sbc), cp, sizeof(cp), &cpLength) == 0); + Require(sceCesUtf8ToSbc(cp, cpLength, &cpLength, cyrillic, &back) == 0 && back == sbc); + } + Require(Utf8ToSbc(cyrillic, "\xC3\xA9", 2) == unassignedCode); + Require(Utf8ToSbc(profile, "\xD0\x90", 2) == unassignedCode); + + char context[256] = {}; + Require(sceCesMbcsUcsContextInit(context, cyrillic) == 0); + Require(sceCesMbcsUcsContextInit(nullptr, cyrillic) == invalidParameter); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..1490f39cd 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -44,6 +44,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication - [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - [sceAudioInInit](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 without library-wide setup, as `sceAudioOutInit` does; each `sceAudioInOpen` opens its own SDL capture device +- [sceCesMbcsUcsContextInit](../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns success without writing to the context, and returns the invalid-parameter error for a null context. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); the context layout and its size are unknown, so nothing is stored. PPSA15246 calls it and imports no other function that takes the context; no conversion through an initialized context exists. `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8` and `sceCesUtf8ToSbc` only, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), where byte 0x98 (unassigned) maps to U+0098 like the unassigned Cp1252 bytes; the console behavior for that byte is not verified ### Unknown function info From 029430690fdbb1307597237754a56cd733b6d618 Mon Sep 17 00:00:00 2001 From: Alberto Ruiz Date: Sun, 11 Oct 2026 16:01:12 +0200 Subject: [PATCH 012/318] test(json2): destroy the value before constructing over it ValueClear constructs a Value, then calls the Value(const Object&) constructor on the same object. The constructor allocates a new node and overwrites the pointer, so the first node (56 bytes) is never freed. LeakSanitizer reports it; destroy the value first. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01R5WJS4ZdQynAJesCZKH5hb --- core/libs/tests/GuestJson.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 8150bc249..147ac9094 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -384,6 +384,7 @@ static void ValueClear() { _ZN3sce4Json6StringC1EPKc(&key, "prop"); Value* item = _ZN3sce4Json6ObjectixERKNS0_6StringE(&obj, &key); _ZN3sce4Json5Value3setEl(item, 999); + _ZN3sce4Json5ValueD1Ev(&value); _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(&value, &obj); Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeObject); _ZN3sce4Json5Value5clearEv(&value); From f133aaa4b00f2bf75e94dc078626aca96c2d8366 Mon Sep 17 00:00:00 2001 From: QRMOE Date: Sun, 11 Oct 2026 16:23:53 +0200 Subject: [PATCH 013/318] fix(libc): check dirname result length Find the directory component before checking the result buffer limit, matching FreeBSD 11.4. Preserve input and guest errno on success; overlong directory results still report ENAMETOOLONG. AI-assisted: yes --- core/libs/prx/libc/src/StringExtensions.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index eaa15adc5..8ea34819c 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -46,27 +46,25 @@ char* APS5_VABI dirname_nid_postfix(const char* path) { } auto length = std::strlen(path); - if (length >= sizeof(buffer)) { - errno = 63; - return nullptr; - } - std::memcpy(buffer, path, length + 1); - while (length > 0 && buffer[length - 1] == '/') --length; + while (length > 0 && path[length - 1] == '/') --length; if (length == 0) { buffer[0] = '/'; buffer[1] = '\0'; return buffer; } - buffer[length] = '\0'; - auto separator = length; - while (separator > 0 && buffer[separator - 1] != '/') --separator; + while (separator > 0 && path[separator - 1] != '/') --separator; if (separator == 0) { buffer[0] = '.'; buffer[1] = '\0'; return buffer; } - while (separator > 1 && buffer[separator - 1] == '/') --separator; + while (separator > 1 && path[separator - 1] == '/') --separator; + if (separator >= sizeof(buffer)) { + errno = 63; + return nullptr; + } + std::memcpy(buffer, path, separator); buffer[separator] = '\0'; return buffer; } From 563b4738a5e2ddfd97a03998035981966930615b Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:24:42 +0400 Subject: [PATCH 014/318] fix(hw-oracle): invalidate cached builds when inputs change --- tools/hw-oracle/hw_oracle.py | 33 ++++-- tools/hw-oracle/test_hw_oracle.py | 167 ++++++++++++++++++++++++++++-- 2 files changed, 185 insertions(+), 15 deletions(-) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 16cc7e87a..c0be67bf5 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -1,5 +1,7 @@ import argparse +import hashlib import os +import shlex import shutil import struct import subprocess @@ -33,15 +35,23 @@ def tool(name): def oracle(): - binary = CACHE / "oracle" source = HERE / "oracle.c" - if binary.exists() and binary.stat().st_mtime >= source.stat().st_mtime: + compiler = shlex.split(os.environ.get("CC", "cc")) + if not compiler: + raise ValueError("CC must name a compiler command") + root = rocm_root() + if root is not None: + root = root.resolve() + configuration = repr((compiler, str(root))).encode("utf-8") + key = hashlib.sha256(source.read_bytes() + configuration).hexdigest() + directory = CACHE / key + binary = directory / "oracle" + if binary.is_file(): return binary - CACHE.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(dir=CACHE) as tmp: + directory.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=directory) as tmp: output = Path(tmp) / "oracle" - command = [os.environ.get("CC", "cc"), "-O1", str(source), "-o", str(output)] - root = rocm_root() + command = [*compiler, "-O1", str(source), "-o", str(output)] if root: lib = root / "lib" command += [f"-I{root / 'include'}", f"-L{lib}", f"-Wl,-rpath,{lib}"] @@ -51,8 +61,12 @@ def oracle(): @cache +def gpu_target(): + return subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() + + def target(): - return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() + return os.environ.get("HW_ORACLE_TARGET") or gpu_target() def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): @@ -78,6 +92,11 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): rows = [tuple(r) for r in rows] + for index, row in enumerate(rows, 1): + try: + struct.pack("<4I", *row) + except struct.error as error: + raise ValueError(f"row {index} must contain four unsigned 32-bit integers") from error wave = 64 if wave64 else 32 padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) env = dict(os.environ) diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 606517706..8cf1dd492 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -1,7 +1,10 @@ import contextlib import io import os +import shlex +import shutil import subprocess +import sys import tempfile import unittest from pathlib import Path @@ -38,24 +41,26 @@ class OracleCacheTests(unittest.TestCase): with self.assertRaises(subprocess.CalledProcessError): hw_oracle.oracle() self.assertEqual(binary.read_bytes() if binary.exists() else None, previous) - self.assertEqual(list(cache.iterdir()), [binary] if previous is not None else []) + self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary] if previous is not None else []) compiler.side_effect = succeed - self.assertEqual(hw_oracle.oracle(), binary) - self.assertEqual(binary.read_bytes(), b"complete executable") - self.assertEqual(hw_oracle.oracle(), binary) + built = hw_oracle.oracle() + self.assertEqual(built.read_bytes(), b"complete executable") + self.assertEqual(hw_oracle.oracle(), built) self.assertEqual(compiler.call_count, 2) - self.assertEqual(list(cache.iterdir()), [binary]) + self.assertEqual(set(file for file in cache.rglob("*") if file.is_file()), {built, binary} if previous is not None else {built}) + if previous is not None: + self.assertEqual(binary.read_bytes(), previous) def test_overlapping_builds_do_not_expose_partial_output(self): with tempfile.TemporaryDirectory() as tmp: cache = Path(tmp) / "cache" - binary = cache / "oracle" outputs = [] def compile(command, *, check): self.assertTrue(check) output = Path(command[command.index("-o") + 1]) outputs.append(output) + binary = output.parent.parent / "oracle" output.write_bytes(b"partial executable") if len(outputs) == 1: self.assertFalse(binary.exists()) @@ -64,11 +69,11 @@ class OracleCacheTests(unittest.TestCase): with patch.object(hw_oracle, "CACHE", cache), patch.object(hw_oracle, "rocm_root", return_value=None), \ patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: - self.assertEqual(hw_oracle.oracle(), binary) + binary = hw_oracle.oracle() self.assertEqual(binary.read_bytes(), b"complete executable") self.assertEqual(compiler.call_count, 2) self.assertNotEqual(outputs[0], outputs[1]) - self.assertEqual(list(cache.iterdir()), [binary]) + self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary]) class FloatModeTests(unittest.TestCase): @@ -122,6 +127,152 @@ class FloatModeTests(unittest.TestCase): self.assertEqual(assemble.call_args.kwargs, MODES | {"lds": 4096}) +class OracleConfigurationTests(unittest.TestCase): + @unittest.skipUnless(sys.platform.startswith("linux"), "Linux compiler and shared-library fixture") + def test_cache_and_compiler_command_with_host_toolchain(self): + compiler = shutil.which("cc") + if compiler is None: + self.skipTest("C compiler not found") + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + runtime = root / "runtime" + (runtime / "lib").mkdir(parents=True) + stub = root / "runtime.c" + stub.write_text("void runtime_stub(void) {}\n", encoding="utf-8") + subprocess.run([compiler, "-shared", "-fPIC", str(stub), "-o", str(runtime / "lib/libhsa-runtime64.so")], check=True) + source = root / "source" + source.mkdir() + source_file = source / "oracle.c" + source_file.write_text('#include \nint main(void) { printf("%d", VALUE); }\n', encoding="utf-8") + os.utime(source_file, (0, 0)) + wrapper = root / "compiler wrapper" + wrapper.write_text("#!/bin/sh\nexec " + shlex.quote(compiler) + ' "$@"\n', encoding="utf-8") + wrapper.chmod(0o755) + with patch.object(hw_oracle, "HERE", source), patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=runtime): + with patch.dict(os.environ, {"CC": shlex.quote(str(wrapper)) + " -DVALUE=7"}): + first = hw_oracle.oracle() + self.assertEqual(subprocess.check_output([first]), b"7") + self.assertEqual(hw_oracle.oracle(), first) + with patch.dict(os.environ, {"CC": shlex.quote(str(wrapper)) + " -DVALUE=8"}): + second = hw_oracle.oracle() + self.assertNotEqual(second, first) + self.assertEqual(subprocess.check_output([second]), b"8") + source_file.write_text('#include \nint main(void) { printf("%d", VALUE + 1); }\n', encoding="utf-8") + os.utime(source_file, (0, 0)) + third = hw_oracle.oracle() + self.assertNotEqual(third, second) + self.assertEqual(subprocess.check_output([third]), b"9") + + def test_compiler_command_supports_wrapper_and_flags(self): + with tempfile.TemporaryDirectory() as tmp: + def compile(command, *, check): + self.assertEqual(command[:3], ["ccache", "cc", "-fno-omit-frame-pointer"]) + Path(command[command.index("-o") + 1]).write_bytes(b"executable") + + with patch.object(hw_oracle, "CACHE", Path(tmp)), patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.dict(os.environ, {"CC": "ccache cc -fno-omit-frame-pointer"}), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"executable") + + def test_source_change_with_preserved_timestamp_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "source" + source.mkdir() + source_file = source / "oracle.c" + source_file.write_text("first", encoding="utf-8") + os.utime(source_file, (0, 0)) + + def compile(command, *, check): + Path(command[command.index("-o") + 1]).write_bytes(source_file.read_bytes()) + + with patch.object(hw_oracle, "HERE", source), patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + self.assertEqual(hw_oracle.oracle().read_bytes(), b"first") + source_file.write_text("second", encoding="utf-8") + os.utime(source_file, (0, 0)) + self.assertEqual(hw_oracle.oracle().read_bytes(), b"second") + self.assertEqual(compiler.call_count, 2) + + def test_compiler_change_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + def compile(command, *, check): + Path(command[command.index("-o") + 1]).write_bytes(command[0].encode()) + + with patch.object(hw_oracle, "CACHE", Path(tmp)), patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + with patch.dict(os.environ, {"CC": "cc"}): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"cc") + with patch.dict(os.environ, {"CC": "clang"}): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"clang") + self.assertEqual(compiler.call_count, 2) + + def test_runtime_path_change_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + + def compile(command, *, check): + library_path = next(option for option in command if option.startswith("-L")) + Path(command[command.index("-o") + 1]).write_bytes(library_path.encode()) + + with patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + with patch.object(hw_oracle, "rocm_root", return_value=root / "one"): + first = hw_oracle.oracle() + self.assertEqual(first.read_bytes(), ("-L" + str(root / "one/lib")).encode()) + with patch.object(hw_oracle, "rocm_root", return_value=root / "two"): + second = hw_oracle.oracle() + self.assertNotEqual(second, first) + self.assertEqual(second.read_bytes(), ("-L" + str(root / "two/lib")).encode()) + self.assertEqual(compiler.call_count, 2) + + def test_target_override_is_read_for_each_call(self): + reset = getattr(hw_oracle.target, "cache_clear", lambda: None) + reset() + self.addCleanup(reset) + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1036"}): + self.assertEqual(hw_oracle.target(), "gfx1036") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): + self.assertEqual(hw_oracle.target(), "gfx1100") + + def test_override_does_not_replace_cached_gpu_detection(self): + hw_oracle.gpu_target.cache_clear() + self.addCleanup(hw_oracle.gpu_target.cache_clear) + with patch.dict(os.environ), patch.object(hw_oracle, "oracle", return_value="oracle"), \ + patch.object(hw_oracle.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, "gfx1036\n")) as query: + os.environ.pop("HW_ORACLE_TARGET", None) + self.assertEqual(hw_oracle.target(), "gfx1036") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): + self.assertEqual(hw_oracle.target(), "gfx1100") + self.assertEqual(hw_oracle.target(), "gfx1036") + query.assert_called_once() + + def test_invalid_rows_fail_before_assembler_or_gpu_lookup(self): + rows = [[(1, 2, 3)], [(1, 2, 3, 4, 5)], [(-1, 0, 0, 0)], [(1 << 32, 0, 0, 0)], [(0.5, 0, 0, 0)]] + with patch.object(hw_oracle, "assemble") as assemble, patch.object(hw_oracle, "oracle") as oracle: + for invalid in rows: + with self.subTest(rows=invalid), self.assertRaisesRegex(ValueError, "row"): + hw_oracle.run("s_nop 0", invalid, **MODES) + assemble.assert_not_called() + oracle.assert_not_called() + + def test_integer_index_values_are_preserved(self): + class Dword: + def __index__(self): + return 0xffffffff + + def dispatch(command, *, check, env): + self.assertEqual(Path(command[3]).read_bytes()[:16], b"\xff" * 4 + bytes(12)) + Path(command[4]).write_bytes(bytes(int(command[2]) * 64)) + + with patch.object(hw_oracle, "assemble", return_value=Path("kernel.co")), \ + patch.object(hw_oracle, "oracle", return_value="oracle"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): + self.assertEqual(hw_oracle.run("s_nop 0", [(Dword(), 0, 0, 0)], **MODES), [(0,) * 16]) + + class GroupSegmentTests(unittest.TestCase): def test_wave64_dispatches_at_most_512_rows(self): rows = [(index, 0, 0, 0) for index in range(1025)] From 97409a6d0827104c00e839cbbcbd656e909071f4 Mon Sep 17 00:00:00 2001 From: QRMOE Date: Sun, 11 Oct 2026 16:25:14 +0200 Subject: [PATCH 015/318] test(libc): cover dirname result length boundaries Cover 16 paths with long leaves, exact-fit and overlong directory results, repeated separators, input preservation and guest errno. The regression reports 12 failures on unchanged main and passes with the accompanying fix. AI-assisted: yes --- core/libs/tests/GuestStrings.cpp | 47 ++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 3f9efbb6c..4ae9225c7 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -119,7 +119,52 @@ static void CheckSscanfS() { #endif } +static bool CheckDirnameLengths() { + const std::string longLeaf(4096, 'x'); + const std::string longestParent(1023, 'p'); + const std::string tooLongParent(1024, 'q'); + struct Case { + std::string path; + const char* expected; + }; + const Case cases[] = { + {std::string(1023, 'x'), "."}, + {std::string(1024, 'x'), "."}, + {longLeaf, "."}, + {"/" + longLeaf, "/"}, + {"one/" + longLeaf, "one"}, + {"/one/" + longLeaf, "/one"}, + {"one//" + longLeaf + "///", "one"}, + {"/one//" + longLeaf + "///", "/one"}, + {longLeaf + "///", "."}, + {std::string(4096, '/'), "/"}, + {longestParent + "/leaf", longestParent.c_str()}, + {longestParent + "//leaf///", longestParent.c_str()}, + {longestParent + "/", "."}, + {tooLongParent + "/leaf", nullptr}, + {tooLongParent + "//leaf///", nullptr}, + {longLeaf + "/leaf", nullptr}, + }; + bool passed = true; + for (const auto& test : cases) { + const std::string original = test.path; + *__error_nid_postfix() = 77; + const char* result = dirname_nid_postfix(test.path.c_str()); + const bool matches = test.expected + ? result != nullptr && std::strcmp(result, test.expected) == 0 && *__error_nid_postfix() == 77 + : result == nullptr && *__error_nid_postfix() == 63; + if (!matches || test.path != original) { + std::fprintf(stderr, "dirname input length %zu: expected output length %zu, received %s, errno %d\n", + test.path.size(), test.expected ? std::strlen(test.expected) : 0, + result ? "a directory" : "null", *__error_nid_postfix()); + passed = false; + } + } + return passed; +} + int main() { + if (!CheckDirnameLengths()) return 1; CheckBoundsCheckedFunctions(); CheckSscanfS(); Require(std::strcmp(dirname_nid_postfix(nullptr), ".") == 0); @@ -142,8 +187,6 @@ int main() { Require(std::strcmp(dirname_nid_postfix(rootChild), "/") == 0); char onlySlashes[] = "////"; Require(std::strcmp(dirname_nid_postfix(onlySlashes), "/") == 0); - const std::string longDirnamePath(1024, 'x'); - Require(dirname_nid_postfix(longDirnamePath.c_str()) == nullptr && *__error_nid_postfix() == 63); Require(std::strcmp(basename_nid_postfix(nullptr), ".") == 0); Require(std::strcmp(basename_nid_postfix(""), ".") == 0); Require(std::strcmp(basename_nid_postfix("////"), "/") == 0); From 4d0d3c06e8c73ed9fa25187d26f5c50b17573878 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Sun, 11 Oct 2026 16:30:37 +0200 Subject: [PATCH 016/318] fix(recompiler): read NumWorkgroups in every mesh shader so RADV sizes its user data RADV (Mesa 26.2.4, GFX12) updates the user SGPR count and draw-id flag it uses for a mesh dispatch only when the bound mesh shader has the grid-size/draw-id slot. A mesh shader reading neither NumWorkgroups nor DrawIndex keeps the previous vertex pipeline's values: the dispatch opens a 3-register SET_SH_REG, writes one value, and the CP parses the following DISPATCH_MESH_DIRECT as register data. The gfx ring then times out and the GPU is reset with VRAM lost. Kena: Bridge of Spirits (PPSA01802) hits this on its layered volume draws, which interleave with vertex draws in one command buffer. Confirmed with RADV_DEBUG=hang (the dispatch header shows up as SPI_SHADER_USER_DATA_GS_4) and the CP header history in the amdgpu devcoredump. Every mesh shader now loads NumWorkgroups.x and sets zero vertices when it is 0, which no dispatch produces, so RADV gives it the grid-size slot and writes as many values as it announces. Co-Authored-By: Claude Opus 5.5 --- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 14 +++++++++++++- docs/dev/TechnicalDebt.md | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 5884af305..4a2f01c8b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -642,6 +642,10 @@ void DefineMeshOutputs(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.meshPrimitives, spv::DecorationBuiltIn, spv::BuiltInPrimitiveTriangleIndicesEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationBuiltIn, spv::BuiltInCullPrimitiveEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationPerPrimitiveEXT); + if (state.numWorkgroupsVariable == 0u) { + state.numWorkgroupsVariable = DefineInterfaceVariable(state, TypeU32Vector(state, 3u), spv::StorageClassInput, "gl_NumWorkGroups"); + state.module.AddAnnotation(spv::OpDecorate, state.numWorkgroupsVariable, spv::DecorationBuiltIn, spv::BuiltInNumWorkgroups); + } } void EmitMeshEntryPoint(SpirvEmitterState& state) { @@ -666,7 +670,15 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { state.module.AddFunction(spv::OpExtInst, TypeU32(state), result, GlslStd450(state), GLSLstd450UMin, value, ConstantU32(state, limit)); return result; }; - const auto vertices = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 0u)), mesh.maxVertices); + const auto groupsX = state.module.AllocateId(); + const auto groupsXPointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeU32(state)), groupsXPointer, state.numWorkgroupsVariable, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpLoad, TypeU32(state), groupsX, groupsXPointer); + const auto noGroups = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), noGroups, groupsX, ConstantU32(state, 0u)); + const auto allocatedVertices = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 0u)), mesh.maxVertices); + const auto vertices = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, TypeU32(state), vertices, noGroups, ConstantU32(state, 0u), allocatedVertices); const auto primitives = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 1u)), mesh.maxPrimitives); state.module.AddFunction(spv::OpSetMeshOutputsEXT, vertices, primitives); const auto anyVertex = state.module.AllocateId(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..72556d878 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -449,6 +449,7 @@ Throughout the project, every function at every stage either **does exactly what - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. - On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1); other such reads throw. - [NGG mesh entry](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) uses the guest wave size (32 or 64) for wave indices, per-wave vertex and primitive counts, and the workgroup wave count in `s3`. The four-bit wave fields follow Mesa's [get_wave_id_in_tg, get_tgsize and get_thread_id_in_tg](https://chromium.googlesource.com/chromiumos/third_party/mesa/+/refs/heads/stabilize-13982.70.B-chromeos-amd/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c); partial waves and groups exceeding 15 waves throw. Indexed and non-indexed wave32 mesh rendering with 32, 64 and 128 threads was checked by pixel readback on Windows with an NVIDIA RTX 4070 Laptop GPU. The guest register values have not been compared with console hardware. +- [Mesh shader grid size](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - every mesh shader loads `NumWorkgroups.x` and declares no vertices when it is 0, which no dispatch produces. The read is a workaround for RADV (Mesa 26.2.4, seen on an RX 9070 XT, GFX12): binding a mesh pipeline whose shader reads neither `NumWorkgroups` nor `DrawIndex` keeps the user SGPR count and draw-id flag of the vertex pipeline bound before it in the same command buffer (`radv_bind_pre_rast_shader` updates them only when the shader has the `AC_UD_VS_BASE_VERTEX_START_INSTANCE` slot), so `radv_emit_userdata_mesh` opens a 3-register `SET_SH_REG` and writes one value, the CP takes the following `DISPATCH_MESH_DIRECT` as register data and the GPU hangs (gfx ring timeout, MODE1 reset). Kena: Bridge of Spirits (PPSA01802) hits it on its layered volume draws. Reading `NumWorkgroups` gives every mesh shader the grid-size slot, so the count and the values match; drop the read once a fixed Mesa is the minimum - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. - Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it: tessellation, mesh and rect-list pipelines, primitives other than triangles, more than 32 fragment input locations and interfaces beyond the device's geometry limits throw. From 31de08300677569b840e0a0b772259eee14ef5f0 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Sun, 11 Oct 2026 16:30:37 +0200 Subject: [PATCH 017/318] test(agc): require every compiled mesh program to read NumWorkgroups Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index c92bd7fad..99955da54 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -2,6 +2,7 @@ #include "Recompiler.hpp" #include "execution/VulkanTestDevice.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include #include #include #include @@ -133,6 +134,26 @@ struct MeshDraw { VkCullModeFlags cull = VK_CULL_MODE_NONE; }; +bool ReadsNumWorkgroups(const std::vector& words) { + constexpr std::uint32_t OpDecorate = 71; + constexpr std::uint32_t OpAccessChain = 65; + constexpr std::uint32_t OpLoad = 61; + constexpr std::uint32_t DecorationBuiltIn = 11; + constexpr std::uint32_t BuiltInNumWorkgroups = 24; + std::uint32_t variable = 0; + std::vector pointers; + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + const auto opcode = words[at] & 0xffffu; + if (length == 0 || at + length > words.size()) return false; + if (opcode == OpDecorate && length == 4 && words[at + 2] == DecorationBuiltIn && words[at + 3] == BuiltInNumWorkgroups) variable = words[at + 1]; + if (opcode == OpAccessChain && length >= 4 && variable != 0 && words[at + 3] == variable) pointers.push_back(words[at + 2]); + if (opcode == OpLoad && length >= 4 && variable != 0 && (words[at + 3] == variable || std::find(pointers.begin(), pointers.end(), words[at + 3]) != pointers.end())) return true; + at += length; + } + return false; +} + void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { const auto target = device.Target(); std::vector userData(12, 0u); @@ -149,6 +170,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { ShaderRecompiler::GraphicsCompileContext{0, {}, setup.mesh, std::nullopt, {setup.draw.indexAddress, setup.draw.indexCount, setup.draw.indexSize, setup.draw.instanceCount}} }; const auto meshResult = ShaderRecompiler::Recompile(geometry); + Require(ReadsNumWorkgroups(meshResult.spirv), "the mesh program does not read NumWorkgroups, so RADV can keep a previous vertex pipeline's user SGPR count for its dispatch"); const auto meshPush = static_cast(meshResult.pushConstants.size()); Require((meshPush == 0) == (setup.geometryPushBytes == 0), "the geometry program's push data does not follow its layout"); From e7c20b30331e2c5be96c34586849b8b4e4bfc640 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 14:35:01 +0000 Subject: [PATCH 018/318] feat(libc): export isnanf and __signbitl --- core/libs/prx/libc/src/Math.cpp | 5 +++++ core/libs/tests/GuestMath.cpp | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9d6a8aebd..ba60360b1 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -169,6 +169,7 @@ short APS5_VABI _FDtest_nid_postfix(const float* value) { return Finite; } int APS5_VABI __isnanf_nid_postfix(float x) { return std::isnan(x) ? 1 : 0; } +int APS5_VABI isnanf_nid_postfix(float x) { return __isnanf_nid_postfix(x); } int APS5_VABI __fpclassifyf_nid_postfix(float x) { constexpr int Infinite = 0x01, NotANumber = 0x02, Normal = 0x04, Subnormal = 0x08, Zero = 0x10; std::uint32_t bits; @@ -180,6 +181,10 @@ int APS5_VABI __fpclassifyf_nid_postfix(float x) { return Normal; } int APS5_VABI __signbitf_nid_postfix(float x) { return std::signbit(x) ? 1 : 0; } +int APS5_VABI __signbitl_nid_postfix(long double x) { + static_assert(sizeof(long double) == 16, "guest long double requires x87 extended precision storage"); + return std::signbit(x) ? 1 : 0; +} static std::mutex g_randLock; static std::uint32_t g_randState = 1; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6d56671b5..01fdbd1f8 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -37,6 +37,8 @@ extern LibcFloatConstant _FInf_nid_postfix; extern LibcFloatConstant _FNan_nid_postfix; short APS5_VABI _FDtest_nid_postfix(const float*); int APS5_VABI __fpclassifyf_nid_postfix(float); +int APS5_VABI isnanf_nid_postfix(float); +int APS5_VABI __signbitl_nid_postfix(long double); float APS5_VABI fmodf_nid_postfix(float, float); float APS5_VABI asinf_nid_postfix(float); float APS5_VABI acosf_nid_postfix(float); @@ -328,6 +330,21 @@ static void CheckFloatClassification() { Require(_FDtest_nid_postfix(reinterpret_cast(&_FNan_nid_postfix)) == 2); } +static void CheckMissingClassificationExports() { + const float infinity = std::numeric_limits::infinity(); + const float nan = std::numeric_limits::quiet_NaN(); + Require(isnanf_nid_postfix(nan) == 1); + Require(isnanf_nid_postfix(infinity) == 0 && isnanf_nid_postfix(-infinity) == 0); + Require(isnanf_nid_postfix(0.f) == 0 && isnanf_nid_postfix(-1.f) == 0); + + const long double negativeNan = std::copysign(std::numeric_limits::quiet_NaN(), -1.L); + Require(__signbitl_nid_postfix(0.L) == 0 && __signbitl_nid_postfix(-0.L) == 1); + Require(__signbitl_nid_postfix(1.L) == 0 && __signbitl_nid_postfix(-1.L) == 1); + Require(__signbitl_nid_postfix(std::numeric_limits::infinity()) == 0); + Require(__signbitl_nid_postfix(-std::numeric_limits::infinity()) == 1); + Require(__signbitl_nid_postfix(negativeNan) == 1); +} + static void CheckDoubleRounding(double value, std::int64_t expected, bool invalid) { for (const bool useLlround : {false, true}) { std::feclearexcept(FE_ALL_EXCEPT); @@ -352,6 +369,7 @@ static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) int main() { CheckFloatClassification(); + CheckMissingClassificationExports(); CheckIntegerConversions(); Require(atof_nid_postfix(" -12.5tail") == -12.5); char* end = nullptr; From 1b9179501f8fa95c303eb0ec00adfffc3c30d2dc Mon Sep 17 00:00:00 2001 From: QRMOE Date: Sun, 11 Oct 2026 16:51:42 +0200 Subject: [PATCH 019/318] fix(libc): preserve strftime literal conversions Normalize FreeBSD 11.4 literal conversions before host formatting. AI-assisted: yes --- core/libs/prx/libc/src/Time.cpp | 68 ++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index b730a9066..1da075245 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -107,24 +107,41 @@ bool isFreeBsdConversion(char specifier) { return specifier == 'k' || specifier == 'l' || specifier == 's' || specifier == 'v' || specifier == '+'; } -bool isModifiedPercent(const char* afterPercent) { - return (afterPercent[0] == 'E' || afterPercent[0] == 'O') && afterPercent[1] == '%'; -} +struct FormatConversion { + std::size_t length; + char specifier; + bool literal; + bool padded; +}; -std::size_t freeBsdConversionLength(const char* afterPercent, char& conversion) { +FormatConversion parseConversion(const char* afterPercent) { const char* cursor = afterPercent; - if ((*cursor == 'E' || *cursor == 'O') && isFreeBsdConversion(cursor[1])) ++cursor; - if (!isFreeBsdConversion(*cursor)) return 0; - conversion = *cursor; - return static_cast(cursor - afterPercent) + 1; + bool alternative = false; + bool padded = false; + while (*cursor != '\0') { + if (*cursor == 'E' || *cursor == 'O') { + if (alternative) break; + alternative = true; + } else if (*cursor == '-' || *cursor == '_' || *cursor == '0') { + if (padded) break; + padded = true; + } else { + break; + } + ++cursor; + } + if (*cursor == '\0') + return {static_cast(cursor - afterPercent), cursor[-1], true, padded}; + const bool literal = *cursor == '%' || std::strchr("aAbBcCdDeFgGhHIjklmMnprRsStTuUVvWwXxyYzZ+", *cursor) == nullptr; + return {static_cast(cursor - afterPercent) + 1, *cursor, literal, padded}; } -bool hasFreeBsdConversion(const char* format) { +bool requiresFreeBsdExpansion(const char* format) { while (*format != '\0') { - if (*format++ != '%' || *format == '\0') continue; - char conversion = 0; - if (freeBsdConversionLength(format, conversion) != 0) return true; - format += isModifiedPercent(format) ? 2 : 1; + if (*format++ != '%') continue; + const auto conversion = parseConversion(format); + if (conversion.literal || (!conversion.padded && isFreeBsdConversion(conversion.specifier))) return true; + format += conversion.length; } return false; } @@ -142,26 +159,25 @@ std::string expandFreeBsdConversions(const char* format, const GuestTm& guest) { std::string result; while (*format != '\0') { const char next = *format++; - if (next != '%' || *format == '\0') { + if (next != '%') { result += next; continue; } - if (isModifiedPercent(format)) { - result += '%'; - result += *format++; - result += *format++; + const auto conversion = parseConversion(format); + if (conversion.literal) { + if (conversion.specifier == '%') result += '%'; + result += conversion.specifier; + format += conversion.length; continue; } - char conversion = 0; - const std::size_t length = freeBsdConversionLength(format, conversion); - if (length == 0) { - result += '%'; - result += *format++; + if (conversion.padded || !isFreeBsdConversion(conversion.specifier)) { + result.append(format - 1, conversion.length + 1); + format += conversion.length; continue; } - format += length; + format += conversion.length; char text[32]; - switch (conversion) { + switch (conversion.specifier) { case 'k': std::snprintf(text, sizeof(text), "%2d", guest.tm_hour); result += text; @@ -302,7 +318,7 @@ int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr) { size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr) { const std::tm host = toHostTm(*timeptr); - if (!hasFreeBsdConversion(format)) return std::strftime(str, count, format, &host); + if (!requiresFreeBsdExpansion(format)) return std::strftime(str, count, format, &host); const std::string expanded = expandFreeBsdConversions(format, *timeptr); return std::strftime(str, count, expanded.c_str(), &host); } From 90a14e6a1f52ec82fb3cba8f58c7424ad0b3498d Mon Sep 17 00:00:00 2001 From: QRMOE Date: Sun, 11 Oct 2026 16:52:30 +0200 Subject: [PATCH 020/318] test(libc): cover strftime literal conversions Exercise unknown, repeated and incomplete conversions, percent escaping and buffer boundaries. AI-assisted: yes --- core/libs/tests/GuestTimeConversion.cpp | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index e0027fb55..3edc8418d 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -180,6 +180,53 @@ static void CheckFreeBsdConversions() { Require(strftime_nid_postfix(tooSmall, sizeof(tooSmall), "%k:%l", &guest) == 0, "strftime accepted an undersized buffer"); } +static void CheckLiteralConversions() { + const std::int64_t timer = 1700000000; + GuestTm guest{}; + Require(gmtime_s_nid_postfix(&timer, &guest) == &guest, "UTC conversion failed"); + struct FormatCase { + const char* format; + const char* expected; + }; + constexpr FormatCase cases[]{ + {"%Q", "Q"}, + {"%P|%f|%N|%1|%#Y", "P|f|N|1|#Y"}, + {"[%Q] %Y-%m-%d", "[Q] 2023-11-14"}, + {"%EQ|%OQ|%_Q|%-Q|%0Q", "Q|Q|Q|Q|Q"}, + {"%E_Q|%_EQ|%O0Q|%-OQ", "Q|Q|Q|Q"}, + {"%EEY|%EOY|%OEY|%OOY", "EY|OY|EY|OY"}, + {"%EE%Y|%OO%k", "E2023|O22"}, + {"%--Y|%__Y|%00Y|%_0Y|%0-Y", "-Y|_Y|0Y|0Y|-Y"}, + {"%E_EY|%_EOY|%O0OY", "EY|OY|OY"}, + {"%", "%"}, + {"text%", "text%"}, + {"%E", "E"}, + {"%O", "O"}, + {"%_", "_"}, + {"%-", "-"}, + {"%0", "0"}, + {"%E_", "_"}, + {"%_E", "E"}, + {"%Q%", "Q%"}, + {"%k%", "22%"}, + {"%%Q|%%%Q|%%%%Q", "%Q|%Q|%%Q"}, + {"%E%Q|%O%Q|%_%Q|%0%Q|%-%Q", "%Q|%Q|%Q|%Q|%Q"}, + {"%E%%Q|%O%%%Q", "%Q|%%Q"}, + {"%Q|%Ek|%Ol|%Ev|%Y", "Q|22|10|14-Nov-2023|2023"}, + {"%\xc3\xa5", "\xc3\xa5"}, + {"%%", "%"}, + {"literal text", "literal text"}, + }; + for (const auto& test : cases) { + CheckFormat(guest, test.format, test.expected); + CheckBufferSize(guest, test.format, std::strlen(test.expected)); + } + CheckFormat(guest, "", ""); + char buffer[2]{'x', 'y'}; + Require(strftime_nid_postfix(buffer, 0, "%Q", &guest) == 0, "strftime accepted a zero-size buffer"); + Require(buffer[0] == 'x' && buffer[1] == 'y', "strftime wrote to a zero-size buffer"); +} + int main() { #ifdef _WIN32 _putenv_s("TZ", "UTC-2"); @@ -233,6 +280,7 @@ int main() { && std::strcmp(formatted, "1970-01-01 00:00:00") == 0, "strftime did not read the guest tm"); CheckFreeBsdConversions(); + CheckLiteralConversions(); CheckConcurrent(libc_gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent libc_gmtime returned another thread's date"); CheckConcurrent(gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent gmtime returned another thread's date"); From 3f4438dccad035a7bbf5d0b972287a211b526e55 Mon Sep 17 00:00:00 2001 From: Ori Shamir Date: Sun, 11 Oct 2026 17:46:24 +0300 Subject: [PATCH 021/318] fix(pad): keep partial L2/R2 values from a controller An SDL controller sets the L2/R2 bit whenever its trigger axis is above zero, and BuildData then reported 255 for any set bit, so a partly pressed trigger read as fully pressed. Report the analog value whenever there is one; 255 remains for an injected press and for a bit without an analog value. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePad/src/PadState.cpp | 7 +++++-- core/libs/tests/GuestPad.cpp | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 1f40fc26a..ca01712ee 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -116,8 +116,11 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin data.left_stick_y = state.sticks[1]; data.right_stick_x = state.sticks[2]; data.right_stick_y = state.sticks[3]; - data.analog_buttons_l2 = std::max(state.analogButtonsL2, (data.buttons & 0x100) != 0 ? 255 : 0); - data.analog_buttons_r2 = std::max(state.analogButtonsR2, (data.buttons & 0x200) != 0 ? 255 : 0); + const auto trigger = [&](std::uint8_t analog, std::uint32_t bit) -> std::uint8_t { + return (injected & bit) != 0 || (analog == 0 && (state.buttons & bit) != 0) ? 255 : analog; + }; + data.analog_buttons_l2 = trigger(state.analogButtonsL2, 0x100); + data.analog_buttons_r2 = trigger(state.analogButtonsR2, 0x200); const bool live = state.hasMotion && output.motionEnabled; const std::array rest{0.0f, 9.80665f, 0.0f}; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 992f0f7b9..c7de7b59b 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -101,6 +101,23 @@ static void CheckTouchContact() { Pad::ReadState(); } +static void CheckPartialTrigger() { + PadInputState partial; + partial.buttons = static_cast(Pad::PadButton::L2) | static_cast(Pad::PadButton::R2); + partial.analogButtonsL2 = 1; + partial.analogButtonsR2 = 73; + PadPublishInput_nid_postfix(partial); + const auto data = Pad::ReadState(); + Require(data.buttons == partial.buttons); + Require(data.analog_buttons_l2 == 1 && data.analog_buttons_r2 == 73); + PadInputState digital; + digital.buttons = static_cast(Pad::PadButton::L2); + PadPublishInput_nid_postfix(digital); + Require(Pad::ReadState().analog_buttons_l2 == 255); + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); +} + static void CheckReadQueue(int handle) { PadData drained[64]{}; scePadRead_nid_postfix(handle, drained, 64); @@ -189,6 +206,7 @@ int main() { Require(scePadGetHandle(user, 2, 0) == handle); CheckTiltCorrection(handle); CheckTouchContact(); + CheckPartialTrigger(); CheckReadQueue(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); From a06728b56699f9989ad319f124d96814805fa8ce Mon Sep 17 00:00:00 2001 From: QRMOE Date: Sun, 11 Oct 2026 16:53:00 +0200 Subject: [PATCH 022/318] docs(libc): update remaining strftime gaps Record literal handling while retaining unimplemented padding and timezone gaps. AI-assisted: yes --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..af16a841b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -573,7 +573,7 @@ Throughout the project, every function at every stage either **does exactly what - [Temporary app-content storage](../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. - [Guest path resolution](../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. - [localtime, mktime](../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule; outside that range `localtime` returns null and `mktime` returns -1, where the console converts. `gmtime` converts every time whose year fits an `int` on both hosts. -- [strftime](../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does, and passes every other conversion to the host's `strftime`. On Windows the conversions the CRT lacks (`%P` and the `%-`, `%_` and `%0` padding flags) still produce nothing, and the padding flags are not supported on the five new conversions at all (`%-k`, `%_k` and `%0k` go to the host), an unknown conversion, a doubled `E` or `O` modifier (`%EE`) or a trailing lone `%` returns 0 where FreeBSD prints `%` and the character, and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null. +- [strftime](../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does. Unknown conversions, repeated modifiers and incomplete conversions emit literals as in FreeBSD 11; recognized conversions otherwise go to the host's `strftime`. On Windows the `%-`, `%_` and `%0` padding flags still produce nothing, including on the five expanded conversions (`%-k`, `%_k` and `%0k` go to the host), and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null. - [LINE_STIPPLE_TEX](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. From cf1195ad20d6fc83a2d0ea6fe25c9effc7af53c9 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:31:39 +0400 Subject: [PATCH 023/318] fix(hw-oracle): narrow cache PR and normalize runtime paths --- tools/hw-oracle/hw_oracle.py | 13 ++------- tools/hw-oracle/test_hw_oracle.py | 48 ++----------------------------- 2 files changed, 5 insertions(+), 56 deletions(-) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index c0be67bf5..46b59c202 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -60,13 +60,11 @@ def oracle(): return binary + + @cache -def gpu_target(): - return subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() - - def target(): - return os.environ.get("HW_ORACLE_TARGET") or gpu_target() + return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): @@ -92,11 +90,6 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): rows = [tuple(r) for r in rows] - for index, row in enumerate(rows, 1): - try: - struct.pack("<4I", *row) - except struct.error as error: - raise ValueError(f"row {index} must contain four unsigned 32-bit integers") from error wave = 64 if wave64 else 32 padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) env = dict(os.environ) diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 8cf1dd492..7de671b40 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -221,57 +221,13 @@ class OracleConfigurationTests(unittest.TestCase): patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: with patch.object(hw_oracle, "rocm_root", return_value=root / "one"): first = hw_oracle.oracle() - self.assertEqual(first.read_bytes(), ("-L" + str(root / "one/lib")).encode()) + self.assertEqual(first.read_bytes(), ("-L" + str((root / "one").resolve() / "lib")).encode()) with patch.object(hw_oracle, "rocm_root", return_value=root / "two"): second = hw_oracle.oracle() self.assertNotEqual(second, first) - self.assertEqual(second.read_bytes(), ("-L" + str(root / "two/lib")).encode()) + self.assertEqual(second.read_bytes(), ("-L" + str((root / "two").resolve() / "lib")).encode()) self.assertEqual(compiler.call_count, 2) - def test_target_override_is_read_for_each_call(self): - reset = getattr(hw_oracle.target, "cache_clear", lambda: None) - reset() - self.addCleanup(reset) - with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1036"}): - self.assertEqual(hw_oracle.target(), "gfx1036") - with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): - self.assertEqual(hw_oracle.target(), "gfx1100") - - def test_override_does_not_replace_cached_gpu_detection(self): - hw_oracle.gpu_target.cache_clear() - self.addCleanup(hw_oracle.gpu_target.cache_clear) - with patch.dict(os.environ), patch.object(hw_oracle, "oracle", return_value="oracle"), \ - patch.object(hw_oracle.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, "gfx1036\n")) as query: - os.environ.pop("HW_ORACLE_TARGET", None) - self.assertEqual(hw_oracle.target(), "gfx1036") - with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): - self.assertEqual(hw_oracle.target(), "gfx1100") - self.assertEqual(hw_oracle.target(), "gfx1036") - query.assert_called_once() - - def test_invalid_rows_fail_before_assembler_or_gpu_lookup(self): - rows = [[(1, 2, 3)], [(1, 2, 3, 4, 5)], [(-1, 0, 0, 0)], [(1 << 32, 0, 0, 0)], [(0.5, 0, 0, 0)]] - with patch.object(hw_oracle, "assemble") as assemble, patch.object(hw_oracle, "oracle") as oracle: - for invalid in rows: - with self.subTest(rows=invalid), self.assertRaisesRegex(ValueError, "row"): - hw_oracle.run("s_nop 0", invalid, **MODES) - assemble.assert_not_called() - oracle.assert_not_called() - - def test_integer_index_values_are_preserved(self): - class Dword: - def __index__(self): - return 0xffffffff - - def dispatch(command, *, check, env): - self.assertEqual(Path(command[3]).read_bytes()[:16], b"\xff" * 4 + bytes(12)) - Path(command[4]).write_bytes(bytes(int(command[2]) * 64)) - - with patch.object(hw_oracle, "assemble", return_value=Path("kernel.co")), \ - patch.object(hw_oracle, "oracle", return_value="oracle"), \ - patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): - self.assertEqual(hw_oracle.run("s_nop 0", [(Dword(), 0, 0, 0)], **MODES), [(0,) * 16]) - class GroupSegmentTests(unittest.TestCase): def test_wave64_dispatches_at_most_512_rows(self): From c2f16e0e4d4b43ba510eaaa3e6d55efc20d74fe4 Mon Sep 17 00:00:00 2001 From: Darryl Fernandes Date: Sat, 10 Oct 2026 19:20:43 -0400 Subject: [PATCH 024/318] fix(libkernel): support RUSAGE_CHILDREN in getrusage Co-authored-by: milescoviello --- core/libs/prx/libkernel/System/src/Process.cpp | 4 ++++ core/libs/tests/GuestGetrusage.cpp | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 45528cd7a..205067781 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -361,6 +361,10 @@ int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { if (usage == nullptr) throw std::invalid_argument("getrusage: usage is null"); + if (who == -1) { + *usage = {}; + return 0; + } if (who != 0 && who != 1) throw std::invalid_argument("getrusage: unsupported who"); #ifdef _WIN32 diff --git a/core/libs/tests/GuestGetrusage.cpp b/core/libs/tests/GuestGetrusage.cpp index a549808cf..da998f3e4 100644 --- a/core/libs/tests/GuestGetrusage.cpp +++ b/core/libs/tests/GuestGetrusage.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include struct Timeval { @@ -28,6 +29,11 @@ static void SpinUntil(int who, std::int64_t micros) { } } int main() { + ResourceUsage children; + std::memset(&children, 0xff, sizeof(children)); + Require(getrusage_nid_postfix(-1, &children) == 0); + const ResourceUsage empty{}; + Require(std::memcmp(&children, &empty, sizeof(children)) == 0); SpinUntil(0, 400000); const std::int64_t mainThread = CpuMicros(1); Require(mainThread >= 200000); From d71c51c22d8123253be438f172a2bbf8c1f41f26 Mon Sep 17 00:00:00 2001 From: Lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:53:21 +0200 Subject: [PATCH 025/318] feat(libkernel): implement wait through guest waitpid --- core/libs/prx/libkernel/System/src/Process.cpp | 4 ++++ core/libs/tests/GuestWaitpid.cpp | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 45528cd7a..32037a80a 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -281,6 +281,10 @@ int APS5_VABI waitpid_nid_postfix(int pid, int* status, int options) { return -1; } +int APS5_VABI wait_nid_postfix(int* status) { + return waitpid_nid_postfix(-1, status, 0); +} + int APS5_VABI execvp_nid_postfix(const char* file, char* const* arguments) { (void)file; (void)arguments; diff --git a/core/libs/tests/GuestWaitpid.cpp b/core/libs/tests/GuestWaitpid.cpp index 235c57d91..8ef90e7b1 100644 --- a/core/libs/tests/GuestWaitpid.cpp +++ b/core/libs/tests/GuestWaitpid.cpp @@ -2,6 +2,7 @@ #include #include extern "C" { +int APS5_VABI wait_nid_postfix(int*); int APS5_VABI waitpid_nid_postfix(int, int*, int); int APS5_VABI getpid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); @@ -28,4 +29,12 @@ int main() { for (int options : {0x40, 0x41, 0x80, 0x100, 0x40000000, static_cast(0x80000040u), static_cast(0xffffffffu)}) RequireFailure(pid, options, invalidArgument); } + int status = untouched; + *__error_nid_postfix() = invalidArgument; + Require(wait_nid_postfix(&status) == -1); + Require(*__error_nid_postfix() == noChild); + Require(status == untouched); + *__error_nid_postfix() = invalidArgument; + Require(wait_nid_postfix(nullptr) == -1); + Require(*__error_nid_postfix() == noChild); } From b443760ca41c2853938a0833a095681fa97d4a68 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:55:09 +0400 Subject: [PATCH 026/318] fix(relinker): validate file-backed unwind record ranges --- core/relinker/CMakeLists.txt | 15 ++++++ .../UnusedNidFilter/EhFrameReader.cpp | 43 ++++++++++++++--- .../tests/UnwindRecordBoundsTests.cpp | 47 +++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..bad6afa79 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -370,6 +370,21 @@ if(BUILD_TESTING) else() message(STATUS "Python3 interpreter not found; skipping Python relinker tests") endif() + add_test_executable(unwind_record_bounds_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/UnwindRecordBoundsTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ) + target_include_directories(unwind_record_bounds_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME unwind_record_bounds COMMAND unwind_record_bounds_tests) + set_tests_properties(unwind_record_bounds PROPERTIES TIMEOUT 20) + add_test_executable(windows_icon_resource_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsIconResourceTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index 84d8b64d2..8ce7c297b 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -1,6 +1,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -37,7 +38,7 @@ public: for (std::uint64_t index = 0; index < count; ++index) { const auto begin = encoded(position, tableEncoding, segment.MappedAddress); const auto fde = encoded(position, tableEncoding, segment.MappedAddress); - if (position > segment.MappedAddress + segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position); + if (position < segment.MappedAddress || position - segment.MappedAddress > segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position); result.push_back(readFde(begin, fde)); } } @@ -51,11 +52,38 @@ private: const std::set& importSlots; std::map cies; + VirtualAddress checkedRangeEnd(VirtualAddress address, std::uint64_t size) const { + if (size > std::numeric_limits::max() - address) + throw RelinkerException("Strict filter: unwind range overflowed", address); + const auto end = address + size; + while (address < end) { + bool found = false; + for (const auto& segment : headers) { + if (segment.Type != 1 || address < segment.MappedAddress) continue; + const auto relative = address - segment.MappedAddress; + if (relative >= segment.FileSize) continue; + const auto chunk = std::min(end - address, segment.FileSize - relative); + if (segment.Offset > bytes.size() || relative > bytes.size() - segment.Offset || + chunk > bytes.size() - segment.Offset - relative) + throw RelinkerException("Strict filter: unwind data exceeds file", address); + address += chunk; + found = true; + break; + } + if (!found) throw RelinkerException("Strict filter: unwind range is not file-backed", address); + } + return end; + } + template TValue read(VirtualAddress& address) const { for (const auto& segment : headers) { if (segment.Type != 1 || address < segment.MappedAddress || address - segment.MappedAddress > segment.FileSize || sizeof(TValue) > segment.FileSize - (address - segment.MappedAddress)) continue; - const auto offset = segment.Offset + address - segment.MappedAddress; + const auto relative = address - segment.MappedAddress; + if (segment.Offset > bytes.size() || relative > bytes.size() - segment.Offset || + sizeof(TValue) > std::numeric_limits::max() - address) + throw RelinkerException("Strict filter: unwind data range overflowed", address); + const auto offset = segment.Offset + relative; if (offset > bytes.size() || sizeof(TValue) > bytes.size() - offset) throw RelinkerException("Strict filter: unwind data exceeds file", address); TValue value; std::memcpy(&value, bytes.data() + offset, sizeof(value)); @@ -118,7 +146,7 @@ private: auto position = address; const auto length = read(position); if (length == 0xFFFFFFFF || length < 8) throw RelinkerException("Strict filter: unsupported CIE record size", address); - const auto end = position + length; + const auto end = checkedRangeEnd(position, length); if (read(position) != 0) throw RelinkerException("Strict filter: invalid CIE identifier", address); const auto version = read(position); if (version != 1 && version != 3 && version != 4) throw RelinkerException("Strict filter: unsupported CIE version", address); @@ -140,8 +168,9 @@ private: if (augmentation.front() != 'z') throw RelinkerException("Strict filter: unsupported CIE augmentation", address); cie.Augmented = true; const auto size = leb(position); + if (position > end || size > end - position) + throw RelinkerException("Strict filter: CIE augmentation exceeds record", address); const auto augmentationEnd = position + size; - if (augmentationEnd > end) throw RelinkerException("Strict filter: CIE augmentation exceeds record", address); for (std::size_t index = 1; index < augmentation.size(); ++index) { switch (augmentation[index]) { case 'L': cie.LsdaEncoding = read(position); break; @@ -166,7 +195,7 @@ private: auto position = address; const auto length = read(position); if (length == 0xFFFFFFFF || length < 8) throw RelinkerException("Strict filter: unsupported FDE record size", address); - const auto end = position + length; + const auto end = checkedRangeEnd(position, length); const auto cieField = position; const auto cieOffset = read(position); if (cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address); @@ -179,6 +208,8 @@ private: if (cie.Personality != 0) region.ExtraTargets.push_back(cie.Personality); if (cie.Augmented) { const auto augmentationSize = leb(position); + if (position > end || augmentationSize > end - position) + throw RelinkerException("Strict filter: FDE augmentation exceeds record", address); const auto augmentationEnd = position + augmentationSize; if (augmentationEnd > end) throw RelinkerException("Strict filter: FDE augmentation exceeds record", address); if (cie.LsdaEncoding != 0xFF) { @@ -201,7 +232,7 @@ private: if ((callEncoding & 0xF0) != 0) throw RelinkerException("Strict filter: unsupported LSDA call-site encoding", address); const auto tableSize = leb(position); if (tableSize > bytes.size()) throw RelinkerException("Strict filter: invalid LSDA table size", address); - const auto tableEnd = position + tableSize; + const auto tableEnd = checkedRangeEnd(position, tableSize); while (position < tableEnd) { encoded(position, callEncoding); encoded(position, callEncoding); diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp new file mode 100644 index 000000000..0210073af --- /dev/null +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Unwind record validation failed"); } + +static std::vector Fixture() { + std::vector bytes(128, 0); + bytes[0] = 1; bytes[1] = 0; bytes[2] = 3; bytes[3] = 0; + Io::WriteU64(bytes, 4, 0x1040); Io::WriteU32(bytes, 12, 1); + Io::WriteU64(bytes, 16, 0x2000); Io::WriteU64(bytes, 24, 0x1060); + Io::WriteU32(bytes, 64, 9); bytes[72] = 1; bytes[73] = 0; bytes[74] = 1; bytes[75] = 0x78; bytes[76] = 16; + Io::WriteU32(bytes, 96, 20); Io::WriteU32(bytes, 100, 36); + Io::WriteU64(bytes, 104, 0x2000); Io::WriteU64(bytes, 112, 0x10); + return bytes; +} + +int main() { + try { + Domain::ProgramHeader load{}; load.Type = 1; load.MappedAddress = 0x1000; load.FileSize = 128; + auto eh = load; eh.Type = 0x6474E550; eh.FileSize = 32; + const auto regions = Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {load, eh}, {}, {}); + Require(regions.size() == 1 && regions[0].Begin == 0x2000 && regions[0].End == 0x2010); + auto second = load; second.MappedAddress = 0x1048; second.Offset = 72; second.FileSize = 56; + auto first = load; first.FileSize = 72; + Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {first, second, eh}, {}, {}).size() == 1); + for (int kind = 0; kind < 7; ++kind) { + auto bytes = Fixture(); auto segment = load; + switch (kind) { + case 0: Io::WriteU32(bytes, 64, 0x1000); break; + case 1: Io::WriteU32(bytes, 96, 0x1000); break; + case 2: Io::WriteU32(bytes, 64, 100); break; + case 3: Io::WriteU32(bytes, 96, 32); bytes.resize(120); break; + case 4: segment.Offset = std::numeric_limits::max() - 4095; segment.MappedAddress = 0; segment.FileSize = 0x1100; break; + case 5: segment.FileSize = 119; break; + case 6: Io::WriteU32(bytes, 64, 8); break; + } + bool rejected = false; + try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, {segment, eh}, {}, {}); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + } + std::cout << "9 unwind record cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} From 1e40599f2490eb11cf273a84a490420824425adc Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sun, 11 Oct 2026 19:17:25 +0300 Subject: [PATCH 027/318] test(libkernel): verify app info missing metadata errors --- core/libs/tests/GuestAppInfo.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index e39662613..534995aa2 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -35,8 +36,11 @@ int main(int argc, char** argv) { if (argc == 3 && std::string_view(argv[1]) == "--without-param-json") { std::filesystem::current_path(argv[2]); AppInfo info{}; - sceKernelGetAppInfo(getpid_nid_postfix(), &info); - return 0; + try { sceKernelGetAppInfo(getpid_nid_postfix(), &info); } + catch (const std::runtime_error& error) { + return std::string_view(error.what()) == "param.json not found" ? 0 : 1; + } + return 1; } const auto previous = std::filesystem::current_path(); const auto root = EnterTemporaryRoot("guest_app_info_"); @@ -63,6 +67,6 @@ int main(int argc, char** argv) { #else const std::string command = arguments; #endif - Require(std::system(command.c_str()) != 0); + Require(std::system(command.c_str()) == 0); std::filesystem::remove_all(empty); } From 216ced6c27f5d02eb83dbe0d147f555289da5914 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 14:54:31 +0300 Subject: [PATCH 028/318] fix(libc): return positive zero when strtod, strtof, strtold and wide variants perform no conversion --- core/libs/prx/libc/src/Strings.cpp | 24 +++++++++++++++++------- core/libs/tests/GuestMath.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 978a499aa..d16874fd5 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -45,6 +45,14 @@ auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) { return value; } +template +TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, char**)) { + char* parsedEnd = nullptr; + const TFloat value = parse(text, &parsedEnd); + if (end != nullptr) *end = parsedEnd; + return parsedEnd == text ? TFloat{0} : value; +} + } extern "C" { @@ -165,14 +173,16 @@ unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr } double APS5_VABI strtod_nid_postfix(const char* str, char** endptr) { - return std::strtod(str, endptr); + return ParseFloat(str, endptr, std::strtod); } -double APS5_VABI atof_nid_postfix(const char* str) { return std::atof(str); } -float APS5_VABI strtof_nid_postfix(const char* str, char** endptr) { return std::strtof(str, endptr); } +double APS5_VABI atof_nid_postfix(const char* str) { return strtod_nid_postfix(str, nullptr); } +float APS5_VABI strtof_nid_postfix(const char* str, char** endptr) { + return ParseFloat(str, endptr, std::strtof); +} long double APS5_VABI strtold_nid_postfix(const char* str, char** endptr) { static_assert(sizeof(long double) == 16, "Guest long double requires x87 extended precision storage"); - return std::strtold(str, endptr); + return ParseFloat(str, endptr, std::strtold); } int APS5_VABI atoi_nid_postfix(const char* str) { @@ -433,17 +443,17 @@ char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, size_t n) { } double APS5_VABI wcstod_nid_postfix(const char16_t* str, char16_t** endptr) { - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return std::strtod(text, end); }); + return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtod); }); } float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr) { - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return std::strtof(text, end); }); + return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtof); }); } long double APS5_VABI wcstold_nid_postfix(const char16_t* str, char16_t** endptr) { static_assert(sizeof(long double) == 16); static_assert(std::numeric_limits::digits == 64); - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return std::strtold(text, end); }); + return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtold); }); } long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, int base) { diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6d56671b5..6a532b0a1 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -11,6 +11,10 @@ #include extern "C" { double APS5_VABI atof_nid_postfix(const char*); +double APS5_VABI strtod_nid_postfix(const char*, char**); +double APS5_VABI wcstod_nid_postfix(const char16_t*, char16_t**); +float APS5_VABI wcstof_nid_postfix(const char16_t*, char16_t**); +long double APS5_VABI wcstold_nid_postfix(const char16_t*, char16_t**); float APS5_VABI strtof_nid_postfix(const char*, char**); long double APS5_VABI strtold_nid_postfix(const char*, char**); std::int64_t APS5_VABI strtol_nid_postfix(const char*, char**, int); @@ -350,8 +354,34 @@ static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) } } +void CheckNoConversionIsPositiveZero() { + for (const char* text : {"-.", "-", "-x", "-.e1", "-e5", "- 1"}) { + char* end = nullptr; + const double parsed = strtod_nid_postfix(text, &end); + Require(parsed == 0. && !std::signbit(parsed) && end == text); + const float parsedFloat = strtof_nid_postfix(text, &end); + Require(parsedFloat == 0.f && !std::signbit(parsedFloat) && end == text); + const long double parsedLong = strtold_nid_postfix(text, &end); + Require(parsedLong == 0.L && !std::signbit(parsedLong) && end == text); + Require(!std::signbit(atof_nid_postfix(text))); + } + char* end = nullptr; + const char negativeZero[] = "-0.0x"; + const double parsed = strtod_nid_postfix(negativeZero, &end); + Require(parsed == 0. && std::signbit(parsed) && end == negativeZero + 4); + const char16_t wide[] = u"-."; + char16_t* wideEnd = nullptr; + const double parsedWide = wcstod_nid_postfix(wide, &wideEnd); + Require(parsedWide == 0. && !std::signbit(parsedWide) && wideEnd == wide); + const float parsedWideFloat = wcstof_nid_postfix(wide, &wideEnd); + Require(parsedWideFloat == 0.f && !std::signbit(parsedWideFloat) && wideEnd == wide); + const long double parsedWideLong = wcstold_nid_postfix(wide, &wideEnd); + Require(parsedWideLong == 0.L && !std::signbit(parsedWideLong) && wideEnd == wide); +} + int main() { CheckFloatClassification(); + CheckNoConversionIsPositiveZero(); CheckIntegerConversions(); Require(atof_nid_postfix(" -12.5tail") == -12.5); char* end = nullptr; From d4f1303d4750e77ba7eb9e90851792e2ac9aa381 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 15:18:54 +0300 Subject: [PATCH 029/318] test(libc): cover the strtod sign cases for every variant and atof --- core/libs/tests/GuestMath.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6a532b0a1..bf55aa8e0 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -354,8 +354,8 @@ static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) } } -void CheckNoConversionIsPositiveZero() { - for (const char* text : {"-.", "-", "-x", "-.e1", "-e5", "- 1"}) { +static void CheckNoConversionIsPositiveZero() { + for (const char* text : {"", " ", "-", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { char* end = nullptr; const double parsed = strtod_nid_postfix(text, &end); Require(parsed == 0. && !std::signbit(parsed) && end == text); @@ -369,8 +369,23 @@ void CheckNoConversionIsPositiveZero() { const char negativeZero[] = "-0.0x"; const double parsed = strtod_nid_postfix(negativeZero, &end); Require(parsed == 0. && std::signbit(parsed) && end == negativeZero + 4); + for (const char* text : {"-0", "-.0", "-0e5", "-0x0p0", "-0x"}) { + const double keptDouble = strtod_nid_postfix(text, &end); + Require(keptDouble == 0. && std::signbit(keptDouble) && end != text); + const float keptFloat = strtof_nid_postfix(text, &end); + Require(keptFloat == 0.f && std::signbit(keptFloat) && end != text); + const long double keptLong = strtold_nid_postfix(text, nullptr); + Require(keptLong == 0.L && std::signbit(keptLong)); + } + Require(strtod_nid_postfix("-inf", nullptr) == -std::numeric_limits::infinity()); + Require(std::isnan(atof_nid_postfix("nan"))); + const double noEndPointer = strtod_nid_postfix("-.", nullptr); + Require(noEndPointer == 0. && !std::signbit(noEndPointer)); const char16_t wide[] = u"-."; char16_t* wideEnd = nullptr; + const char16_t wideKept[] = u" -0.0x"; + const double keptWide = wcstod_nid_postfix(wideKept, &wideEnd); + Require(keptWide == 0. && std::signbit(keptWide) && wideEnd == wideKept + 5); const double parsedWide = wcstod_nid_postfix(wide, &wideEnd); Require(parsedWide == 0. && !std::signbit(parsedWide) && wideEnd == wide); const float parsedWideFloat = wcstof_nid_postfix(wide, &wideEnd); From 43783f725c93f867f945296f8eafff52c0316fdc Mon Sep 17 00:00:00 2001 From: Lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:28:46 +0200 Subject: [PATCH 030/318] feat(libSceAgc): build versioned ACB atomic GDS commands --- core/libs/prx/libSceAgc/Acb/src/Control.cpp | 13 ++- core/libs/prx/libSceAgc/tests/Commands.cpp | 97 +++++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index de20362b0..a4692c619 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -11,10 +11,15 @@ extern "C" { -// unknown signature -void* APS5_VABI sceAgcAcbAtomicGds_0900(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; +std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { + const auto operation = (control & 0xffu) | ((control & 0x200u) << 7u) | + ((control & 0x20u) << 13u) | ((control & 0x40u) << 14u) | + ((control & 0x20u) == 0 && mode == 1 ? 0x20000u : 0u); + return Agc::Command::Emit(buf, 0x1du, { + operation, ((gdsOp & 1u) << 8u) | (src & 0x3fu), offset, index, + mask & 0x00ff00ffu, data, static_cast(compareData), + static_cast(compareData >> 32u), static_cast(extraData), + static_cast(extraData >> 32u)}, __func__); } std::uint32_t* APS5_VABI sceAgcAcbJump(CommandBuffer* buf, std::uint8_t cachePolicy, const std::uint32_t* target, std::uint32_t sizeInDwords) { diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index fd59ffa94..d3d9c7f83 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -37,6 +37,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr); extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw); extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData); namespace { @@ -404,6 +405,101 @@ void testMemory() { check(storage.words == beforeMask, "invalid mask patch modified packet memory"); } +struct AtomicGdsAllocation { + Storage storage; + std::uint32_t capacity = 15; + std::uint32_t calls = 0; + bool accepted = true; +}; + +bool APS5_VABI growAtomicGds(CommandBuffer* buffer, std::uint32_t count, void* userData) { + auto& allocation = *static_cast(userData); + check(count == 15, "atomic GDS allocation omitted packet or reserved words"); + ++allocation.calls; + if (!allocation.accepted) return false; + buffer->bottom = allocation.storage.words.data(); + buffer->top = buffer->bottom + allocation.capacity; + buffer->cursor_up = buffer->bottom; + buffer->cursor_down = buffer->top; + return true; +} + +void testAtomicGds() { + Storage storage; + storage.words.fill(0xdeadbeefu); + storage.buffer.cursor_up += 2; + auto* packet = sceAgcAcbAtomicGds_0900(&storage.buffer, 0xffffffffu, 1, 0xffffffffu, 0xffffffffu, + 0xabcd, 0xfedc, 0x12345678u, 0x87654321u, 0xfedcba9876543210ull, 0x0123456789abcdefull); + const std::array expected{ + 0xc0091d00u, 0x001500ffu, 0x13fu, 0xabcdu, 0xfedcu, 0x00340078u, + 0x87654321u, 0x76543210u, 0xfedcba98u, 0x89abcdefu, 0x01234567u}; + check(packet == storage.words.data() + 2, "atomic GDS returned the wrong packet address"); + check(std::equal(expected.begin(), expected.end(), packet), "atomic GDS packet or guest ABI mismatch"); + check(storage.buffer.cursor_up == packet + expected.size(), "atomic GDS did not advance by eleven words"); + check(storage.words[0] == 0xdeadbeefu && storage.words[1] == 0xdeadbeefu && + std::all_of(storage.words.begin() + 13, storage.words.end(), [](auto word) { return word == 0xdeadbeefu; }), + "atomic GDS wrote outside its packet"); + + const std::array, 9> controls{{ + {0, 0, 0}, {0, 1, 0x00020000u}, {0, 2, 0}, {0, 0xffffffffu, 0}, + {0x20u, 1, 0x00040020u}, {0x40u, 1, 0x00120040u}, + {0x200u, 1, 0x00030000u}, {0xfffffd00u, 0, 0}, {0xffu, 1, 0x001400ffu}}}; + for (const auto& entry : controls) { + Storage controlStorage; + auto* controlPacket = sceAgcAcbAtomicGds_0900(&controlStorage.buffer, entry[0], entry[1], + 0xfffffffeu, 0xffffffc0u, 0, 0, 0xff00ff00u, 0, 0, 0); + std::array controlExpected{}; + controlExpected[0] = 0xc0091d00u; + controlExpected[1] = entry[2]; + check(std::equal(controlExpected.begin(), controlExpected.end(), controlPacket), "atomic GDS control or field masking mismatch"); + } + + auto emit = [](CommandBuffer* buffer) { + return sceAgcAcbAtomicGds_0900(buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + }; + Storage exact; + exact.buffer.cursor_down = exact.words.data() + 15; + exact.buffer.reserved_dw = 4; + check(emit(&exact.buffer) == exact.words.data() && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS rejected an exact fit with reserved space"); + const auto exactBefore = exact.words; + expectFailure([&] { emit(&exact.buffer); }); + check(exact.words == exactBefore && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS overwrote reserved space"); + Storage exhausted; + exhausted.buffer.cursor_down = exhausted.words.data() + 10; + const auto before = exhausted.words; + expectFailure([&] { emit(&exhausted.buffer); }); + check(exhausted.words == before && exhausted.buffer.cursor_up == exhausted.words.data(), + "failed atomic GDS allocation modified the buffer"); + expectFailure([&] { emit(nullptr); }); + + Storage noDown; + noDown.buffer.top = noDown.words.data() + 11; + noDown.buffer.cursor_down = nullptr; + check(emit(&noDown.buffer) == noDown.words.data() && noDown.buffer.cursor_up == noDown.buffer.top, + "atomic GDS did not honor a buffer with no down cursor"); + + AtomicGdsAllocation allocation; + CommandBuffer growing{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &allocation, 4}; + check(emit(&growing) == allocation.storage.words.data() && allocation.calls == 1 && + growing.cursor_up == allocation.storage.words.data() + 11, "atomic GDS did not use the guest allocation callback"); + const std::array zeroPacket{0xc0091d00u}; + check(std::equal(zeroPacket.begin(), zeroPacket.end(), allocation.storage.words.begin()), + "atomic GDS callback allocation produced the wrong packet"); + for (const bool accepted : {false, true}) { + AtomicGdsAllocation failure; + failure.accepted = accepted; + failure.capacity = 14; + const auto failureBefore = failure.storage.words; + CommandBuffer failed{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &failure, 4}; + expectFailure([&] { emit(&failed); }); + check(failure.calls == 1 && failure.storage.words == failureBefore && + failed.cursor_up == (accepted ? failure.storage.words.data() : nullptr), + "atomic GDS wrote a packet after failed or insufficient allocation"); + } +} + void testDebugRaiseException() { int result = -1; try { @@ -459,6 +555,7 @@ int main(int argc, char** argv) { testRegisterRange(); testPacketPayloadAddress(); testMemory(); + testAtomicGds(); testDebugRaiseException(); testDefaults(); LibcRunShutdown_nid_postfix(); From 9d96752fcd8ec333335d2517a3e2ccb0e7e1fb5e Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:31:11 -0400 Subject: [PATCH 031/318] fix(libkernel): let munmap succeed over a range with nothing mapped munmap and sceKernelMunmap over a range in which no page is mapped failed: munmap returned EINVAL and sceKernelMunmap let the registry's exception escape. Both now return 0. The console returns 0 for sceKernelMunmap of a range it just unmapped and of a never-mapped address, and EINVAL only for a zero length, as measured by prosper's console oracle (kernelx.golden.tsv, kx_fmem_unmap_again, kx_fmem_munmap_unmapped). The other half of #3196, a range with an unmapped page in the middle, is already fixed on main by 04ed61957; the test covers both. Co-Authored-By: Claude Opus 5.5 --- .../libkernel/DirectMemory/DirectMemory.cpp | 1 + core/libs/tests/GuestMemory.cpp | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 1fae6ec14..4f261f154 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -923,6 +923,7 @@ int DoMunmap(void* addr, size_t len) { Trace("unmap %p+0x%zx", addr, len); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || !addr) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; + if (!mutation.Overlaps(addr, len)) return 0; UnmapRegistered(mutation, addr, len); return 0; } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index bfab22098..4897c7008 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1672,6 +1672,28 @@ static void CheckAvailableDirectMemoryIsLargestFreeBlock() { Require(start == top && size == static_cast(window)); } +static void CheckMunmapOverUnmappedPages() { + constexpr std::size_t page = 0x4000; + const auto failed = reinterpret_cast(static_cast(-1)); + void* raw = mmap_nid_postfix(nullptr, page * 3, 3, 0x1002, -1, 0); + Require(raw != failed); + auto* memory = static_cast(raw); + memory[0] = 1; + memory[page * 2] = 2; + Require(munmap_nid_postfix(memory + page, page) == 0); + Require(munmap_nid_postfix(memory, page * 3) == 0); + { + GuestAllocations::Mutation mutation; + Require(!mutation.Overlaps(memory, page * 3)); + } + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(memory, 0, &info, sizeof(info)) == SCE_KERNEL_ERROR_EACCES); + Require(sceKernelVirtualQuery(memory + page * 2, 0, &info, sizeof(info)) == SCE_KERNEL_ERROR_EACCES); + Require(munmap_nid_postfix(memory, page * 3) == 0); + Require(sceKernelMunmap(memory, page * 3) == 0); + Require(sceKernelMunmap(memory, 0) == SCE_KERNEL_ERROR_EINVAL); +} + int main() { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "1000"); @@ -1681,6 +1703,7 @@ int main() { CheckVirtualQueryForNonReadableGuestRange(); CheckReleaseFlexibleMemory(); CheckVirtualQueryPartialMunmap(); + CheckMunmapOverUnmappedPages(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); CheckBatchMapStopsAtInvalidEntry(); @@ -1793,7 +1816,7 @@ int main() { Require(munmap_nid_postfix(memory, page) == 0); Require(memory[page * 2] == 73); Require(munmap_nid_postfix(memory + page * 2, page) == 0); - Require(munmap_nid_postfix(memory, page) == -1); + Require(munmap_nid_postfix(memory, page) == 0); Require(madvise_nid_postfix(memory, page, 4) == 0); *__error_nid_postfix() = 0; Require(mprotect_nid_postfix(memory, page, 1) == -1 && *__error_nid_postfix() == 22); From 38cb3b12d19e999b922890193f99c24933c609eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 18:32:13 +0200 Subject: [PATCH 032/318] docs(technical-debt): record the single source of sceAgcAcbAtomicGds_0900 Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..ece4cccb9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -149,6 +149,7 @@ Throughout the project, every function at every stage either **does exactly what - [Strided buffer overhang](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Zero-stride vertex fetch range](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. +- [sceAgcAcbAtomicGds_0900](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - signature, control/mode bit mapping and the 11-dword ATOMIC_GDS (0x1d) layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, which derives them from the library's disassembly; KytyPS5 and shadPS4 have no body for it. Not confirmed on hardware or a title (PPSA07286 imports it, nothing was run), and the driver has no ATOMIC_GDS handler (opcode 0x1d is not executed as an atomic GDS operation) - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature - [sceAgcDcbContextStateOpGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `sceAgcDcbContextStateOp_0100` writes; KytyPS5 returns 0 for an operation above 3, here it throws From 3a3504850a9124df269316aa8766f1762c75f601 Mon Sep 17 00:00:00 2001 From: A-Akhil Date: Sun, 11 Oct 2026 22:06:22 +0530 Subject: [PATCH 033/318] fix(shader): take v_sin_f32 and v_cos_f32 from an exact series where the driver's result is more than 4 ulp off --- .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index dc3fa2a70..646655287 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -746,6 +746,31 @@ std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { return Select(state, TypeF32(state), Binary(state, spv::OpLogicalAnd, TypeBool(state), near, far), series, value); } +std::uint32_t SinTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0xc17183a8u); + for (const std::uint32_t coefficient : {0x42283c1au, 0xc2996966u, 0x42a335e3u, 0xc2255de7u, 0x40c90fdbu}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, polynomial)); +} + +std::uint32_t CosTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0x40fce9c5u); + for (const std::uint32_t coefficient : {0xc1d368f9u, 0x4270fa83u, 0xc2aae9e4u, 0x4281e0f8u, 0xc19de9e6u, 0x3f800000u}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return polynomial; +} + +std::uint32_t SeriesWhenFar(SpirvEmitterState& state, std::uint32_t value, std::uint32_t series) { + const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); + const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); + const auto distance = EmitExt(state, TypeU32(state), GLSLstd450UMin, {Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), Binary(state, spv::OpISub, TypeU32(state), seriesBits, valueBits)}); + return Select(state, TypeF32(state), Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)), series, value); +} + std::uint32_t TrigLinearNearZero(SpirvEmitterState& state, std::uint32_t value, std::uint32_t distance, std::uint32_t linearConstant) { const auto linear = Binary(state, spv::OpFMul, TypeF32(state), distance, linearConstant); const auto magnitude = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), distance), ConstantU32(state, 0x7fffffffu)); @@ -757,9 +782,11 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, true); const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); const auto signBits = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), cycle), ConstantU32(state, 0x80000000u)); - const auto value = EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}); const auto nearHalf = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e800000u)); - const auto distance = Select(state, TypeF32(state), nearHalf, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute), absolute); + const auto distance = Select(state, TypeF32(state), nearHalf, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)), absolute); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), distance, ConstantF32(state, 0x3e000000u)); + const auto series = Select(state, TypeF32(state), pastEighth, CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), distance))), SinTurnSeries(state, distance)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); const auto linearConstant = Select(state, TypeF32(state), nearHalf, ConstantF32(state, 0x40c90fdbu), ConstantF32(state, 0x40c90fd5u)); const auto result = TrigLinearNearZero(state, value, distance, linearConstant); return Unary(state, spv::OpBitcast, TypeF32(state), Binary(state, spv::OpBitwiseOr, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), result), signBits)); @@ -768,8 +795,14 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { std::uint32_t EmitFPCos(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, false); const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); - const auto value = EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}); - const auto distance = Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute); + const auto distance = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute)); + const auto low = CosTurnSeries(state, absolute); + const auto middle = SinTurnSeries(state, distance); + const auto high = Unary(state, spv::OpFNegate, TypeF32(state), CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)))); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e000000u)); + const auto pastThreeEighths = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3ec00000u)); + const auto series = Select(state, TypeF32(state), pastThreeEighths, high, Select(state, TypeF32(state), pastEighth, middle, low)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); return TrigLinearNearZero(state, value, distance, ConstantF32(state, 0x40c90fdbu)); } From 2f5d166e2beca897b523f6aa40bdcde99c521ed2 Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:34:47 -0400 Subject: [PATCH 034/318] fix(shader): accept output modifiers on v_cvt_i16_f16 and ignore them as the hardware does v_cvt_i16_f16 with mul:2, mul:4, div:2 or clamp in the VOP3 encoding stops the recompiler with "VOP3 source modifiers are not implemented". On gfx1036 (Radeon 610M) the result is identical with and without mul:2, mul:4, div:2 and clamp combined with mul:2, for all 66 measured inputs; clamp on its own is not a measured case. The decoder now accepts omod and clamp on v_cvt_i16_f16 and drops them, as measured. Clamp without an output multiplier is accepted by the same rule: clamp with mul:2 gives the same hardware result as mul:2 alone. Matches 528 of 528 rows in two float modes, 0 differing. v_cvt_u16_f16 is not measured and stays refused. Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../execution/VCvtI16F16OutputModifiers.cpp | 130 ++++++++++++++++++ .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 3 +- 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp new file mode 100644 index 000000000..b4517eaac --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp @@ -0,0 +1,130 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +constexpr std::uint32_t RowCount = 12; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, + 0x7e1402ffu, 0x5a5a5a5au, 0xd5d3000au, 0x00000104u, + 0xe0781000u, 0x80010a03u, 0xbf810000u, +}; + +struct Variant { + const char* modifiers; + std::uint32_t clamp; + std::uint32_t omod; +}; + +constexpr Variant Variants[] = { + {"mul:2", 0, 1}, + {"mul:4", 0, 2}, + {"div:2", 0, 3}, + {"clamp mul:2", 1, 1}, +}; + +constexpr std::uint32_t Rows[RowCount] = { + 0x00003c00u, + 0x0000bc00u, + 0x0000c100u, + 0x00004200u, + 0x00006400u, + 0x00005640u, + 0x0000d640u, + 0x00007bffu, + 0x0000fbffu, + 0x00007e00u, + 0xbeef3c00u, + 0xbeefc100u, +}; + +constexpr std::uint32_t Expected[RowCount] = { + 0x5a5a0001u, 0x5a5affffu, 0x5a5afffeu, 0x5a5a0003u, + 0x5a5a0400u, 0x5a5a0064u, 0x5a5aff9cu, 0x5a5a7fffu, + 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5afffeu, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::ShaderFloatMode& floatMode, const Variant& variant) { + Code[7] = 0xd5d3000au | variant.clamp << 15u; + Code[8] = 0x00000104u | variant.omod << 27u; + Input.fill(0u); + for (std::uint32_t tid = 0; tid < RowCount; ++tid) Input[tid * Inputs] = Rows[tid]; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* mode, const Variant& variant) { + for (std::uint32_t tid = 0; tid < RowCount; ++tid) { + const std::uint32_t actual = Output[tid * Results]; + Require(actual == Expected[tid], std::string("v_cvt_i16_f16 ") + variant.modifiers + " " + mode + ": lane " + std::to_string(tid) + + " of " + Hex(Rows[tid]) + " is " + Hex(actual) + ", expected " + Hex(Expected[tid])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const Variant& variant : Variants) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, variant); + Check("M1", variant); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, variant); + Check("M2", variant); + } + std::puts("v_cvt_i16_f16 output modifiers tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 0e6078d53..eb6cea434 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -2060,8 +2060,9 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, const bool sourceModifiers = supportsNativeVop3SourceModifiers(opcode); const bool resultModifiers = supportsNativeVop3ResultModifiers(opcode); const bool clampModifier = supportsNativeVop3Clamp(opcode); + const bool ignoresOutputModifiers = opcode == RdnaOpcode::VCvtI16F16; if (sourceModifiers) { - if (opSel != 0u || (omod != 0u && !resultModifiers) || (clamp != 0u && !clampModifier)) { + if (opSel != 0u || (omod != 0u && !resultModifiers && !ignoresOutputModifiers) || (clamp != 0u && !clampModifier && !ignoresOutputModifiers)) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } return; From 28962787b39f5a57236d0422ca996a10b2e06493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 18:44:14 +0200 Subject: [PATCH 035/318] docs(technical-debt): describe the v_sin and v_cos series fallback Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..5bb6907a1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -478,7 +478,7 @@ Throughout the project, every function at every stage either **does exactly what - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). -- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. +- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_log_f32 and v_log_f16](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Dual-source blending](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. From f51180973ea6d8d8d48735682647876ecadfaf2f Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 18:22:39 +0200 Subject: [PATCH 036/318] feat(libSceAudioOut): implement sceAudioOutSysGetHdmiMonitorInfo Surround Sound Studio (PPSA99051), catalog build on main a11c8f7f with the flat-interpolation fix, stops 16 s in, before its first frame (orishamir, #3581 comment 6109839314): sceAudioOutSysGetHdmiMonitorInfo not implemented The call takes (type, out, size); out must be non-null, size exactly 0x180 and type 1 (HDMI) or 0x10 (sainsaji/PS5-Audio-Passthrough-Research c3be9c2a, docs/FINDINGS.md section 3 and AGENTS.md:25, firmware 12.70). Titles read the monitor name at +0x0B, the descriptor count at +0x94 and 8-byte descriptors from +0x98 (same, FINDINGS.md hardware log item 1 and sample-app/src/platform/ps5/bitstream_out.cpp:92-128). Audio reaches the host through SDL as PCM and there is no EDID, so type 1 with a 0x180-byte buffer reports a sink without a name or descriptors: the buffer is zeroed and 0 is returned. Surround Sound Studio's Passthrough page then lists no formats; EVO Player's code treats an empty list like a negative return. A null buffer returns 0x80260004, as the other AudioOut calls do. Type 0x10, other types and other sizes throw: their results and error codes are not known. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 10 +++ core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 16 ++++- .../tests/AudioOutSysHdmiMonitorInfo.cpp | 71 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 3 +- 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 968a17129..bc11e8e5b 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -57,6 +57,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_sys_hdmi_monitor_info_tests tests/AudioOutSysHdmiMonitorInfo.cpp) + target_include_directories(audio_out_sys_hdmi_monitor_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_sys_hdmi_monitor_info_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_sys_hdmi_monitor_info_tests) + add_test(NAME audio_out_sys_hdmi_monitor_info COMMAND audio_out_sys_hdmi_monitor_info_tests) + if(WIN32) + set_tests_properties(audio_out_sys_hdmi_monitor_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_latency_tests tests/AudioOut2Latency.cpp) target_include_directories(audio_out2_latency_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_latency_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index ddddb77fb..c8adb77b2 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,8 @@ static constexpr std::uint32_t FORMAT_MASK = 0xFFu; static constexpr std::uint64_t TARGET_LATENCY_US = 40000; static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000; static constexpr std::uint64_t DRAIN_SLEEP_US = 1000; +static constexpr int MONITOR_TYPE_HDMI = 1; +static constexpr std::uint32_t HDMI_MONITOR_INFO_SIZE = 0x180; enum class Format { Unknown, @@ -506,8 +509,17 @@ int APS5_VABI sceAudioOutSysConfigureOutput() { return 0; } -int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int type, void* info, std::uint32_t size) { + if (type != MONITOR_TYPE_HDMI) { + throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: monitor type " + std::to_string(type) + " not supported"); + } + if (size != HDMI_MONITOR_INFO_SIZE) { + throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: size " + std::to_string(size) + " not supported"); + } + if (info == nullptr) { + return -2144993276; + } + std::memset(info, 0, HDMI_MONITOR_INFO_SIZE); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp new file mode 100644 index 000000000..ce23a664d --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int, void*, std::uint32_t); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +namespace { + +constexpr int monitorTypeHdmi = 1; +constexpr std::uint32_t infoSize = 0x180; +constexpr std::size_t descriptorCountOffset = 0x94; +constexpr std::size_t guardSize = 16; +constexpr std::uint8_t untouched = 0xA5; +constexpr int invalidPointer = static_cast(0x80260004); + +bool Throws(int type, std::uint32_t size, const std::string& expected) { + std::array info{}; + try { + sceAudioOutSysGetHdmiMonitorInfo(type, info.data(), size); + } catch (const std::runtime_error& error) { + if (std::string(error.what()) == expected) return true; + std::fprintf(stderr, "threw: %s\n", error.what()); + return false; + } + return false; +} + +} + +int main() { + std::array buffer{}; + buffer.fill(untouched); + Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, buffer.data(), infoSize) == 0, + "an HDMI query with a 0x180-byte buffer must succeed"); + for (std::uint32_t i = 0; i < infoSize; i++) { + Require(buffer[i] == 0, "the monitor info must describe a sink without a name or audio descriptors"); + } + std::uint32_t descriptors = 0xFFFFFFFFu; + std::memcpy(&descriptors, buffer.data() + descriptorCountOffset, sizeof(descriptors)); + Require(descriptors == 0, "the audio descriptor count must be 0"); + for (std::size_t i = infoSize; i < buffer.size(); i++) { + Require(buffer[i] == untouched, "nothing past 0x180 bytes may be written"); + } + + Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, nullptr, infoSize) == invalidPointer, + "a null buffer must report an invalid pointer"); + + Require(Throws(monitorTypeHdmi, infoSize - 1, + "sceAudioOutSysGetHdmiMonitorInfo: size 383 not supported"), "a short buffer must throw"); + Require(Throws(monitorTypeHdmi, infoSize + 1, + "sceAudioOutSysGetHdmiMonitorInfo: size 385 not supported"), "a long buffer must throw"); + Require(Throws(0x10, infoSize, + "sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"), "monitor type 0x10 must throw"); + Require(Throws(0, infoSize, + "sceAudioOutSysGetHdmiMonitorInfo: monitor type 0 not supported"), "monitor type 0 must throw"); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..a6f817c92 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -208,7 +208,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [sceAudioOutGetLastOutputTime](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device -- [sceAudioOutSysClose, sceAudioOutSysConfigureOutput, sceAudioOutSysGetHdmiMonitorInfo, sceAudioOutSysOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; added without an implementation and declared without parameters. [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L166-L240), whose audio code both titles share, calls `sceAudioOutSysGetHdmiMonitorInfo` to read the formats the TV or receiver lists ([bitstream.cpp](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/bitstream.cpp#L71)) and `sceAudioOutSysOpen`, `sceAudioOutSysConfigureOutput` and `sceAudioOutSysClose` only to open a port for HDMI bitstream passthrough. Its declared arguments are not confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no HDMI sink to describe or switch +- [sceAudioOutSysClose, sceAudioOutSysConfigureOutput, sceAudioOutSysOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; added without an implementation and declared without parameters. [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L166-L240), whose audio code both titles share, calls `sceAudioOutSysOpen`, `sceAudioOutSysConfigureOutput` and `sceAudioOutSysClose` only to open a port for HDMI bitstream passthrough. Its declared arguments are not confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no HDMI output to switch - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped @@ -518,6 +518,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [sceImeKeyboardGetInfo](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [AudioOut2 contexts](../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. +- [sceAudioOutSysGetHdmiMonitorInfo](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - [ATRAC9 gapless segments](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame From f0bca23deb9ec9ae598b5c12967db34a8b92f2e9 Mon Sep 17 00:00:00 2001 From: aayushmishraaa Date: Sun, 11 Oct 2026 22:26:47 +0530 Subject: [PATCH 037/318] fix(relinker): reject input executables that are not x86-64 Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/parsing/ElfReader.cpp | 2 ++ .../relinker/relinker/tests/test_elf_header.py | 18 +++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index a92ef6238..e5223a067 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -85,6 +85,8 @@ ElfHeader ElfReader::ReadHeader() const { } if (_fileBuffer[4] != 2 || _fileBuffer[5] != 1 || _fileBuffer[6] != 1) throw RelinkerException("Expected little-endian ELF64 version 1"); + if (_readU16At(0x12) != 62) + throw RelinkerException("Expected an x86-64 ELF"); ElfHeader header{}; header.Machine = _readU16At(0x12); diff --git a/core/relinker/relinker/tests/test_elf_header.py b/core/relinker/relinker/tests/test_elf_header.py index 553665bbd..d31486831 100644 --- a/core/relinker/relinker/tests/test_elf_header.py +++ b/core/relinker/relinker/tests/test_elf_header.py @@ -8,17 +8,21 @@ from test_guest_intel_trampolines import main_fixture def identification_cases(relinker, directory): image = main_fixture() - cases = [("sysv", image, False)] + cases = [("sysv", image, None)] freebsd = bytearray(image) freebsd[7] = 9 - cases.append(("freebsd", freebsd, False)) + cases.append(("freebsd", freebsd, None)) for field, values in ((4, (0, 1, 255)), (5, (0, 2, 255)), (6, (0, 2, 255))): for value in values: data = bytearray(image) data[field] = value - cases.append((f"ident-{field}-{value}", data, True)) + cases.append((f"ident-{field}-{value}", data, "Expected little-endian ELF64 version 1")) + for value in (0, 3, 183): + data = bytearray(image) + data[18:20] = value.to_bytes(2, "little") + cases.append((f"machine-{value}", data, "Expected an x86-64 ELF")) for mode, flags in enumerate(([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"])): - for name, data, invalid in cases: + for name, data, error in cases: source = Path(directory) / f"{mode}-{name}.elf" output = source.with_suffix(".out") registry = output.with_name(output.stem + ".registry.json") @@ -26,8 +30,8 @@ def identification_cases(relinker, directory): result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", *flags, str(source), str(output)], capture_output=True, text=True, timeout=20) assert source.read_bytes() == data, (mode, name, "source changed") - if invalid: - assert result.returncode == 2 and "Expected little-endian ELF64 version 1" in result.stderr, (mode, name, result) + if error: + assert result.returncode == 2 and error in result.stderr, (mode, name, result) assert not output.exists() and not registry.exists(), (mode, name, "output written for unsupported ELF identification") else: assert result.returncode == 0 and registry.is_file(), (mode, name, result) @@ -39,7 +43,7 @@ def main(): cases = [ ("tiny", bytes(10), "File too small for ELF header"), ("truncated", b"\x7fELF\x02\x01\x01" + bytes(25), "File too small for ELF header"), - ("header-only", b"\x7fELF\x02\x01\x01" + bytes(57), "No PT_DYNAMIC segment found"), + ("header-only", b"\x7fELF\x02\x01\x01" + bytes(11) + b"\x3e" + bytes(45), "No PT_DYNAMIC segment found"), ("bad-magic", bytes(64), "Invalid ELF magic number"), ] with tempfile.TemporaryDirectory(prefix="anyps5-elf-header-") as directory: From d7bf16a054d031730646e84f98ad3d87df14bcd0 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:02:18 +0200 Subject: [PATCH 038/318] fix(exports): export the calls PPSA30601 imports PPSA30601 (Resonance: A Plague Tale Legacy) fails to load without these imports that no library exports. Their signatures are unknown, so they are declared without parameters and throw until they are implemented. Adds a TechnicalDebt entry. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAmpr/Export.cpp | 5 ++++ core/libs/prx/libSceJson2/Export.cpp | 30 +++++++++++++++++++ core/libs/prx/libSceMouse/Export.cpp | 5 ++++ .../libs/prx/libSceSaveData.native/Export.cpp | 5 ++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 46 insertions(+) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index c2c1cc811..d54946870 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -1029,4 +1029,9 @@ APS5_EXPORT("qlWiRfOJx1A", sceAmprFpclassifyd); int APS5_VABI sceAmprFpclassifyd(double value) { return SonyFpclassifyd(value); } + +int APS5_VABI sceAmprAmmGetUsageStatsData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 672d857ee..498690ee6 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -830,4 +830,34 @@ int APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json6Object4PairD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Array8iteratorppEi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Array8iteratoraSERKS2_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 9d820d245..157e3afcc 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceMouse/include/MouseState.hpp" +#include "prx/libc/include/General.hpp" extern "C" { @@ -22,4 +23,8 @@ void MousePublishInput_nid_postfix(const MouseInputEvent& event) { Mouse::Publish(event); } +int APS5_VABI sceMouseResetAbsolutePosition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 4f96392ed..a030bac77 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -754,4 +754,9 @@ int APS5_VABI sceSaveDataCancel() { NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI sceSaveDataDirNameSearch2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..d07c877cf 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -597,3 +597,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [six sce::Json::Object::Pair, sce::Json::Object and sce::Json::Array::iterator members](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceAmprAmmGetUsageStatsData](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr), [sceMouseResetAbsolutePosition](../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) and [sceSaveDataDirNameSearch2](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) From ed0ea5e66be27924104107597621049bf9d3a382 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:06:24 +0200 Subject: [PATCH 039/318] docs: place the PPSA30601 TechnicalDebt entry away from the end of the file Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index d07c877cf..8fd34f89c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -591,10 +591,10 @@ Throughout the project, every function at every stage either **does exactly what - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. +- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [six sce::Json::Object::Pair, sce::Json::Object and sce::Json::Array::iterator members](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceAmprAmmGetUsageStatsData](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr), [sceMouseResetAbsolutePosition](../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) and [sceSaveDataDirNameSearch2](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [six sce::Json::Object::Pair, sce::Json::Object and sce::Json::Array::iterator members](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceAmprAmmGetUsageStatsData](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr), [sceMouseResetAbsolutePosition](../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) and [sceSaveDataDirNameSearch2](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) From f4424b5f1ec418cb029df76eaf7607a4e1025a37 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 10:49:33 +0200 Subject: [PATCH 040/318] feat(libSceAgc): implement the vieBRwlh1Lw interpolant mapping update Hollow Knight Silksong (PPSA12544) imports libSceAgc NID vieBRwlh1Lw (#699). Its name is unknown: it is not in aerolib, and the sceAgcUpdateInterpolantMapping label other projects give it hashes to SbuY2jN+axQ. OpenAGC (src/game_compat.c at 3c7e807, sceAgcUnknownVieBRwlh1Lw) reads it from FW 11.60 as the dbOlWdppb4o transform with the same (regs, gs, ps) arguments, without the identity tail: it writes one register per pixel shader input, none for a pixel shader without inputs, and rejects a null pixel shader. It now shares the dbOlWdppb4o path and throws for the rejected arguments. For is_f16 1 and 3 inputs whose default_value and default_value_hi differ, OpenAGC takes DEFAULT_VAL from default_value_hi, while KytyPS5, sharpemu and dbOlWdppb4o here take it from default_value; these inputs throw. On every other input the dbOlWdppb4o values equal OpenAGC's. agc_interpolant_mapping runs the dbOlWdppb4o vectors and OpenAGC's enhanced-mapping vectors through it. Co-Authored-By: Claude Fable 5.1 --- .../Shader/src/InterpolantMapping.cpp | 36 +++++++--- core/libs/prx/libSceAgc/Unimplemented.cpp | 6 -- .../libSceAgc/tests/InterpolantMapping.cpp | 71 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 3 +- 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index b3b0da8f4..608c859ad 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -53,10 +53,21 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS : CreateInterpolantValue(psWord, gsSemantic); } -int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue) { +std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { + const std::uint32_t f16 = (psWord >> 20u) & 0x3u; + if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) { + throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown"); + } + return CreateInterpolantValueSplitF16(psWord, gsSemantic); +} + +int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) { if (regs == nullptr) { throw std::runtime_error(std::string(fn) + ": regs is null"); } + if (ps == nullptr && !fillIdentityTail) { + throw std::runtime_error(std::string(fn) + ": ps is null"); + } if (ps != nullptr && ps->num_input_semantics > 32) { throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32"); } @@ -66,13 +77,14 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* auto* output = regs; regs = values.data(); if (ps == nullptr || ps->num_input_semantics == 0) { - FillIdentityInterpolants(regs, 0); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : 0; + if (fillIdentityTail) FillIdentityInterpolants(regs, 0); if (ps != nullptr) { - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); if (gs != nullptr) AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); } transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -98,11 +110,12 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* SetInterpolantRegister(regs, i, createValue(ShaderSemanticWord(psSemantic), gsSemantic)); } - FillIdentityInterpolants(regs, ps->num_input_semantics); - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : ps->num_input_semantics; + if (fillIdentityTail) FillIdentityInterpolants(regs, ps->num_input_semantics); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -111,12 +124,17 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* extern "C" { int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true); } APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping); int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true); +} + +APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); +int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); } } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 860242c9d..0b85ef7ff 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -90,10 +90,4 @@ int APS5_VABI sceAgcGetShaderInstrumentation() { return 0; } -APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); -int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index 1ea1d6ca2..b176809a4 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -13,12 +13,14 @@ extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { using Registers = std::array; const Shader* mappedPixel = nullptr; Registers preparedMapping{}; +std::size_t preparedCount = 0; unsigned mappings = 0; unsigned links = 0; @@ -65,6 +67,11 @@ void testIdentity() { regs = filled(); check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed"); checkIdentity(regs, 0, "mapping without inputs is not the identity"); + regs = filled(); + check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed"); + check(preparedCount == 0u, "update without inputs prepared registers"); + const auto untouched = filled(); + check(std::memcmp(regs.data(), untouched.data(), sizeof(regs)) == 0, "update without inputs wrote registers"); } void testMapping() { @@ -99,6 +106,15 @@ void testMapping() { check(split[4].value == 0x03180002u, "full f16 interpolant mapping changed"); checkIdentity(split, static_cast(inputs.size()), "unused interpolants are not the identity"); + auto update = filled(); + const auto untouched = filled(); + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(preparedCount == inputs.size(), "update prepared registers past its inputs"); + check(std::memcmp(update.data(), preparedMapping.data(), written) == 0, "prepared update differs from published registers"); + check(std::memcmp(update.data(), split.data(), written) == 0, "update mapping differs from the split f16 mapping"); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + auto regular = filled(); check(sceAgcCreateInterpolantMapping_0100(regular.data(), &gs, &ps) == 0, "mapping failed"); check(std::memcmp(regular.data(), preparedMapping.data(), sizeof(regular)) == 0, "prepared mapping differs from published registers"); @@ -106,6 +122,51 @@ void testMapping() { checkIdentity(regular, static_cast(inputs.size()), "unused interpolants are not the identity"); } +void testUpdateMapping() { + std::array inputs{semantic(1, 0, 0), semantic(2, 0, 1), semantic(3, 0, 2), semantic(4, 0, 3)}; + inputs[0].is_custom = 1; + inputs[0].default_value = 2; + inputs[1].default_value = 1; + inputs[1].default_value_hi = 1; + inputs[2].default_value_hi = 2; + inputs[3].is_custom = 1; + inputs[3].default_value = 3; + inputs[3].default_value_hi = 3; + std::array outputs{semantic(1, 5, 0), semantic(2, 7, 1), semantic(4, 9, 3)}; + Shader gs{}; + gs.output_semantics = outputs.data(); + gs.num_output_semantics = static_cast(outputs.size()); + Shader ps{}; + ps.input_semantics = inputs.data(); + ps.num_input_semantics = static_cast(inputs.size()); + + auto update = filled(); + const auto untouched = filled(); + check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(update[0].value == 0x00000625u, "custom interpolant update changed"); + check(update[1].value == 0x01380107u, "low f16 half update changed"); + check(update[2].value == 0x02480220u, "unmatched high f16 half update changed"); + check(update[3].value == 0x03680709u, "full f16 interpolant update changed"); + for (std::uint32_t i = 0; i < inputs.size(); ++i) { + check(update[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "updated register offset changed"); + } + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + + auto create = filled(); + check(sceAgcUnknownCreateInterpolantMapping(create.data(), &gs, &ps) == 0, "split f16 mapping failed"); + check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping"); + checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); + + for (const std::uint32_t index : {1u, 3u}) { + inputs[index].default_value = 0; + update = filled(); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps); }); + check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); + inputs[index].default_value = inputs[index].default_value_hi; + } +} + void testRejections() { std::array inputs{}; Shader gs{}; @@ -117,18 +178,23 @@ void testRejections() { const auto saved = regs; expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); }); expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); }); check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers"); ps.num_input_semantics = 1; expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); }); expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); }); } } extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) { - check(shader != nullptr && context.size() == preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); + check(shader != nullptr && context.size() <= preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); mappedPixel = shader; std::copy(context.begin(), context.end(), preparedMapping.begin()); + preparedCount = context.size(); ++mappings; } @@ -141,8 +207,9 @@ int main() { try { testIdentity(); testMapping(); + testUpdateMapping(); testRejections(); - check(mappings == 3u && links == 2u, "mapping preparation call count changed"); + check(mappings == 7u && links == 5u, "mapping preparation call count changed"); LibcRunShutdown_nid_postfix(); std::puts("AGC interpolant mapping tests passed"); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..3827dbf51 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -221,7 +221,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceQoSInit](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. The app type and the block are not used, as no endpoint is implemented - AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures - [sceNpSessionSignalingGetConnectionFromPeerAddress](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - imported by PPSA01289 and added without an implementation; unknown signature -- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only - [scePadIsRemoteController](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw @@ -597,3 +597,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` From d1631862f5d4d61323ef73bf9716f2c8fd975c2b Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 14:20:54 -0300 Subject: [PATCH 041/318] fix(libkernel): report the operation mode the console reports sceKernelGetOperationMode returned 0 for both values, which were unknown. A payload on a PS5 (firmware 13.60) reads mode 2 and submode 0; nothing past the two ints is written and the result register holds no status, so the function now returns nothing. PPSA12544 logs both values and ignores the result. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 9 +++++++++ .../libs/prx/libkernel/System/src/Process.cpp | 5 ++--- core/libs/tests/GuestOperationMode.cpp | 20 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestOperationMode.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..feb2f67e2 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -682,6 +682,15 @@ if(BUILD_TESTING) set_tests_properties(guest_sched_priority PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_operation_mode_tests ${LIBS_INCLUDE_DIR}/tests/GuestOperationMode.cpp) + target_include_directories(guest_operation_mode_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_operation_mode_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_operation_mode_tests) + add_test(NAME guest_operation_mode COMMAND guest_operation_mode_tests) + if(WIN32) + set_tests_properties(guest_operation_mode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_waitpid_tests ${LIBS_INCLUDE_DIR}/tests/GuestWaitpid.cpp) target_include_directories(guest_waitpid_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_waitpid_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 45528cd7a..95493d47f 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -453,12 +453,11 @@ int APS5_VABI sceKernelIsTrinityMode(void) { return 0; } -int APS5_VABI sceKernelGetOperationMode(int* mode, int* submode) { +void APS5_VABI sceKernelGetOperationMode(int* mode, int* submode) { if (!mode || !submode) throw std::invalid_argument("sceKernelGetOperationMode: null output"); - *mode = 0; + *mode = 2; *submode = 0; - return 0; } int APS5_VABI seteuid_nid_postfix(std::uint32_t euid) { diff --git a/core/libs/tests/GuestOperationMode.cpp b/core/libs/tests/GuestOperationMode.cpp new file mode 100644 index 000000000..248ec491a --- /dev/null +++ b/core/libs/tests/GuestOperationMode.cpp @@ -0,0 +1,20 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +extern "C" { +void APS5_VABI sceKernelGetOperationMode(int* mode, int* submode); +} +static void Require(bool value) { if (!value) std::abort(); } +static bool Throws(int* mode, int* submode) { + try { sceKernelGetOperationMode(mode, submode); } catch (const std::invalid_argument&) { return true; } + return false; +} +int main() { + int out[4] = {-1, -1, -1, -1}; + sceKernelGetOperationMode(&out[0], &out[2]); + Require(out[0] == 2 && out[1] == -1); + Require(out[2] == 0 && out[3] == -1); + int value = -1; + Require(Throws(nullptr, &value) && Throws(&value, nullptr)); + Require(value == -1); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..1886917fd 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -224,7 +224,7 @@ Throughout the project, every function at every stage either **does exactly what - [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature - [scePadIsRemoteController](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled -- [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw +- [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation; nothing is allocated from these pools, so the peak and current use are 0, where the console already reports use right after initialization (0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool) - [sceNpWebApi2GetMemoryPoolStats](../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to `sceNpWebApi2Initialize`, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws From 2c769288c538a98e5424706a450c5751836eee61 Mon Sep 17 00:00:00 2001 From: jackthereaper2006 <220788941+jackthereaper2006@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:13:40 +0500 Subject: [PATCH 042/318] docs: record DOOM homebrew on Linux with an AMD GPU Co-Authored-By: Claude Opus 5.5 --- docs/user/COMPATIBILITY.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/user/COMPATIBILITY.md b/docs/user/COMPATIBILITY.md index a6eb265f4..27dcd13c3 100644 --- a/docs/user/COMPATIBILITY.md +++ b/docs/user/COMPATIBILITY.md @@ -11,6 +11,7 @@ DOOM Shareware is a public homebrew, [lowbit/ps5-doom](https://github.com/lowbit - Windows 11, RTX 4070 SUPER (v1.1.1, `main` `2edaf7f4` plus open PRs, reported in [#690](https://github.com/boykopovar/AnyPS5/pull/690)): further maps, weapon switching, new games, level select, save/load round trip and audible audio. - Linux, Ubuntu 24.04 container, Mesa llvmpipe, no hardware GPU (v1.1.1, `main` `0e173a46` plus [#1278](https://github.com/boykopovar/AnyPS5/pull/1278), [#1279](https://github.com/boykopovar/AnyPS5/pull/1279) and [#1307](https://github.com/boykopovar/AnyPS5/pull/1307), relinked with `--exclude-sce-module libc.prx`, see [#1497](https://github.com/boykopovar/AnyPS5/issues/1497)): E1M1, 35 FPS in game; audio and extended gameplay not tested. The `/download0` directory must exist beside the executable. - Linux, CachyOS, NVIDIA RTX 3080 Ti, driver 610.57.04, KDE Plasma Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 at 35 FPS in game; new game from the menu, movement, turning, firing, an item pickup and the automap checked with controller input from `APS5_PAD_INPUT_FILE`. Audio output opened at 48000 Hz but was not listened to; later maps and save/load not tested. The `/download0` directory did not need to exist before the first run. +- Linux, CachyOS, AMD Ryzen 5 PRO 5650U integrated graphics (RADV RENOIR), Mesa 26.2.4, kernel 7.2.9, KDE Plasma 6.7.5 Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 and E1M2 completed at 35 FPS in game with a DualShock 4 through SDL (both sticks, D-pad, touchpad, options menu), audio heard, save/load round trip in `/download0`. The first run without a shader cache stayed on AGC compute presentation. `/dev/udmabuf` was accessible to the user, so shared direct memory was imported as dma-buf. - On the first run without a shader cache, the presentation kernel is compiled at its first dispatch, frame 1 does not complete in time and the title switches to the CPU scaler. Later runs with the cached kernel stay on AGC compute presentation (measured on Windows 11, RTX 3080, `main` `d70b8998`). -Not verified: Windows on current `main` without extra PRs, Linux with AMD or Intel GPUs. +Not verified: Windows on current `main` without extra PRs, Linux with Intel GPUs. From 4d15f9ef240a544c2ed9be1453e87465349639b4 Mon Sep 17 00:00:00 2001 From: Zayd Zaari <110066668+zaydzaari@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:36:41 +0000 Subject: [PATCH 043/318] fix(rtc): recognize additional measured named time zones Add thirteen offsets measured repeatedly on PS5 firmware 13.42, keeping DST names ahead of their shorter prefixes. Cover case folding, prefix matching and a second valid date. Written with AI assistance; validated against real hardware and the guest RTC tests. --- core/libs/prx/libSceRtc/Export.cpp | 2 ++ core/libs/tests/GuestRtc.cpp | 29 +++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index e1c13629d..b0f3a5603 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -234,6 +234,8 @@ bool parseZoneName(const char* cursor, std::int64_t& offsetMinutes) { const char* name; std::int64_t minutes; } zones[] = { + {"WETDST", 60}, {"METDST", 120}, {"EETDST", 180}, {"SWT", 60}, {"MEZ", 60}, {"AESST", 660}, {"ACSST", 630}, + {"CAST", 570}, {"CADT", 630}, {"SAST", 570}, {"SADT", 630}, {"WST", 480}, {"WDT", 540}, {"GMT", 0}, {"WET", 0}, {"BST", 60}, {"WAT", -60}, {"AST", -240}, {"ADT", -180}, {"EST", -300}, {"EDT", -240}, {"CST", -360}, {"CDT", -300}, {"MST", -420}, {"MDT", -360}, {"PST", -480}, {"PDT", -420}, {"YST", -540}, {"YDT", -480}, {"HDT", -540}, {"CAT", -600}, {"AHST", -600}, {"NST", -150}, {"NDT", -90}, {"IDLW", -720}, {"CET", 60}, {"MET", 60}, diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 252649a32..d82c887ad 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -299,6 +299,35 @@ int main() { Require(tick.tick == dateTime.local - static_cast(dateTime.minutes * 60000000)); } + const struct { + const char* name; + std::int64_t minutes; + } additionalZones[] = { + {"SWT", 60}, {"MEZ", 60}, {"WETDST", 60}, {"METDST", 120}, {"EETDST", 180}, + {"AESST", 660}, {"ACSST", 630}, {"CAST", 570}, {"CADT", 630}, {"SAST", 570}, {"SADT", 630}, {"WST", 480}, {"WDT", 540}, + }; + for (const auto& zone : additionalZones) { + char text[80]; + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %s", zone.name); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + char lower[16]{}; + for (std::size_t i = 0; zone.name[i] != 0; ++i) lower[i] = static_cast(zone.name[i] + ('a' - 'A')); + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %s", lower); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %sextra", zone.name); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + std::snprintf(text, sizeof(text), "Wed, 01 Mar 2000 08:09:10 %s", zone.name); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == at(2000, 3, 1, 8, 9, 10) - static_cast(zone.minutes * 60000000)); + } + RtcTick source{leapDayTick}; RtcTick result{}; Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..e8e77442f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -100,7 +100,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutGetResolutionStatus](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified -- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console's zone name table is only known from the names measured on a PS5 (firmware 13.60); any other name is assumed to take the military letter offset of its first character, as every measured name outside the table does (`HST`, `MSK`, `XYZ`), so a name the console's table holds but nobody measured gets the wrong offset +- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console's zone name table is only known from the names measured on PS5 firmware 13.60 and 13.42; any other name is assumed to take the military letter offset of its first character, as every measured name outside the table does (`HST`, `MSK`, `XYZ`), so a name the console's table holds but nobody measured gets the wrong offset - [sceAudio3dPortOpen](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [sceAudiodecDecode](../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 - [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF From 7714a5c2dbc38ebe73f31d766dc13c44b089f6f7 Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:43:52 +0200 Subject: [PATCH 044/318] feat(libSceSysmodule): register libSceShare module ID (0x10F) --- core/libs/prx/libSceSysmodule/ModuleTable.hpp | 1 + core/libs/tests/GuestSysmodule.cpp | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index 4840450be..ff1034b0c 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -149,6 +149,7 @@ inline const std::unordered_map kModuleTable = { {0x00000109, "libSceHdrScopes"}, {0x0000010c, "libSceCesCs"}, {0x0000010d, "libScePlayerInvitationDialog"}, + {0x0000010f, "libSceShare"}, {0x00000110, "libSceNpTrophy2"}, {0x00000112, "libSceNpSessionSignaling"}, {0x00000113, "libSceNpEntitlementAccess"}, diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 4a63bbd37..78591db14 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -115,6 +115,13 @@ int main() { Require(sceSysmoduleUnloadModule(kUltModuleId) == 0); Require(sceSysmoduleIsLoaded(kUltModuleId) == kModuleUnloaded); + constexpr std::uint16_t kShareModuleId = 0x010f; + Require(sceSysmoduleIsLoaded(kShareModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kShareModuleId) == 0); + Require(sceSysmoduleIsLoaded(kShareModuleId) == 0); + Require(sceSysmoduleUnloadModule(kShareModuleId) == 0); + Require(sceSysmoduleIsLoaded(kShareModuleId) == kModuleUnloaded); + Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kTrophy2ModuleId) == 0); Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == 0); From 9b1e1ecccd90d32e22710427d7bf7625501e6026 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:12:19 -0700 Subject: [PATCH 045/318] fix(savedata): sort sceSaveDataDirNameSearch results by the requested key and order The search returned saves in host directory order and ignored the condition's key and order. A title that asks for the newest save first (modification time, descending) got whichever directory the host listed first: the alphabetically first on NTFS, an arbitrary one on Linux. Sort by directory name, user parameter or modification time, reverse for a descending order, and reject unknown keys and orders with the parameter error, as shadPS4 does. --- .../libs/prx/libSceSaveData.native/Export.cpp | 42 ++++++++++--- .../prx/libSceSaveData.native/SaveData.hpp | 8 +++ core/libs/tests/GuestSaveDataSearch.cpp | 63 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 4f96392ed..a4475f9f4 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -270,13 +270,22 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS } result->hit_num = 0; result->set_num = 0; + const std::uint32_t key = cond->key; + if ((key > SAVE_DATA_SORT_KEY_MTIME && key != SAVE_DATA_SORT_KEY_FREE_BLOCKS) || cond->order > SAVE_DATA_SORT_ORDER_DESCENT) { + return SAVE_DATA_ERROR_PARAMETER; + } const char* pattern = (cond->dir_name != nullptr) ? cond->dir_name->data : nullptr; const std::string root = save_root(); if (!std::filesystem::is_directory(root)) { return SAVE_DATA_OK; } - std::uint32_t hit = 0; - std::uint32_t set = 0; + struct Match { + std::string name; + std::string path; + SaveDataParam param; + }; + const bool sortByParam = key == SAVE_DATA_SORT_KEY_USER_PARAM || key == SAVE_DATA_SORT_KEY_MTIME; + std::vector matches; for (const auto& entry : std::filesystem::directory_iterator(root)) { if (!entry.is_directory()) { continue; @@ -285,16 +294,29 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if (!dir_name_match(name.c_str(), pattern)) { continue; } - hit++; - if (result->dir_names != nullptr && set < result->dir_names_num) { - std::snprintf(result->dir_names[set].data, sizeof(result->dir_names[set].data), "%s", name.c_str()); - if (result->params != nullptr) { - result->params[set] = load_param(entry.path().string()); - } - set++; + Match match{name, entry.path().string(), {}}; + if (sortByParam) { + match.param = load_param(match.path); + } + matches.push_back(std::move(match)); + } + std::sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.name < b.name; }); + if (key == SAVE_DATA_SORT_KEY_USER_PARAM) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.user_param < b.param.user_param; }); + } else if (key == SAVE_DATA_SORT_KEY_MTIME) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.mtime < b.param.mtime; }); + } + if (cond->order == SAVE_DATA_SORT_ORDER_DESCENT) { + std::reverse(matches.begin(), matches.end()); + } + const std::uint32_t set = result->dir_names == nullptr ? 0 : static_cast(std::min(result->dir_names_num, matches.size())); + for (std::uint32_t i = 0; i < set; i++) { + std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s", matches[i].name.c_str()); + if (result->params != nullptr) { + result->params[i] = sortByParam ? matches[i].param : load_param(matches[i].path); } } - result->hit_num = hit; + result->hit_num = static_cast(matches.size()); result->set_num = set; return SAVE_DATA_OK; } diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 20d02ad5a..ceffb09c0 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -33,6 +33,14 @@ constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_DETAIL = 3; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_USER_PARAM = 4; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_MTIME = 5; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_DIRNAME = 0; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_USER_PARAM = 1; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_BLOCKS = 2; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_MTIME = 3; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_FREE_BLOCKS = 5; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_ASCENT = 0; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_DESCENT = 1; + constexpr std::size_t SAVE_DATA_MOUNT_SLOTS = 16; struct MountSlot { diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index cc24049af..0be7b2d33 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -8,17 +8,79 @@ #include #include #include +#include extern "C" { int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); +int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); } +static constexpr int SaveDataErrorParameter = -2137063424; + static void Require(bool value, const char* message) { if (!value) throw std::runtime_error(message); } +static std::vector Search(std::uint32_t key, std::uint32_t order, std::uint32_t capacity, std::uint32_t* hits) { + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + cond.key = key; + cond.order = order; + std::array names{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = capacity; + Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "sorted search failed"); + *hits = result.hit_num; + std::vector found; + for (std::uint32_t i = 0; i < result.set_num; ++i) found.emplace_back(names[i].data); + return found; +} + +static void SortByKey() { + struct Save { + const char* name; + std::uint32_t userParam; + std::int64_t mtime; + }; + const std::array saves{{{"SORT_B", 1, 1000}, {"SORT_A", 3, 2000}, {"SORT_C", 2, 3000}}}; + for (const auto& save : saves) { + std::filesystem::create_directories(std::filesystem::path("_sd") / save.name); + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", save.name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 2; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "mount for parameters failed"); + Require(sceSaveDataSetParam(&mounted.mount_point, 4, &save.userParam, sizeof(save.userParam)) == 0, "user parameter was not set"); + Require(sceSaveDataSetParam(&mounted.mount_point, 5, &save.mtime, sizeof(save.mtime)) == 0, "mtime was not set"); + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + } + using Names = std::vector; + std::uint32_t hits = 0; + Require(Search(0, 0, 3, &hits) == Names{"SORT_A", "SORT_B", "SORT_C"} && hits == 3, "directory names must sort ascending"); + Require(Search(0, 1, 3, &hits) == Names{"SORT_C", "SORT_B", "SORT_A"}, "directory names must sort descending"); + Require(Search(1, 0, 3, &hits) == Names{"SORT_B", "SORT_C", "SORT_A"}, "user parameters must sort ascending"); + Require(Search(1, 1, 3, &hits) == Names{"SORT_A", "SORT_C", "SORT_B"}, "user parameters must sort descending"); + Require(Search(3, 0, 3, &hits) == Names{"SORT_B", "SORT_A", "SORT_C"}, "modification times must sort ascending"); + Require(Search(3, 1, 1, &hits) == Names{"SORT_C"} && hits == 3, "the newest save must come first in descending modification time"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.key = 6; + SaveDataDirNameSearchResult result{}; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort key must be rejected"); + cond.key = 0; + cond.order = 2; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort order must be rejected"); +} + static void SearchAndMount() { const std::array names{std::string(31, 'A'), std::string(31, 'B')}; for (const auto& name : names) std::filesystem::create_directories(std::filesystem::path("_sd") / name); @@ -64,6 +126,7 @@ int main() { int status = 0; try { SearchAndMount(); + SortByKey(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..d2bd4dca5 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -283,6 +283,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet - [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw +- [sceSaveDataDirNameSearch](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order - [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature - [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature From 896c95823696b99eff1dbb56793e68d22e8f43cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 19:49:46 +0200 Subject: [PATCH 046/318] docs(technical-debt): record that sceSaveDataDirNameSearch rejects sort key 4 Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index d2bd4dca5..592353e69 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -283,7 +283,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet - [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw -- [sceSaveDataDirNameSearch](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order +- [sceSaveDataDirNameSearch](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it - [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature - [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature From 7f0c5d9d4de3f48b2dc789a0f651a046f34ea90d Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Sun, 11 Oct 2026 21:18:29 +0530 Subject: [PATCH 047/318] fix(relinker): accept the input's directory as a module directory Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/guest/GuestModuleBuilder.cpp | 9 ++++-- .../relinker/tests/test_guest_module_dir.py | 29 +++++++++++++++++++ docs/user/USAGE.md | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 7ce382aa7..b5ceae762 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -102,6 +102,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path continue; } if (!entry.is_regular_file()) continue; + if (std::filesystem::equivalent(entry.path(), inputPath)) continue; if (isElf(entry.path())) paths.push_back(entry.path()); } } @@ -288,6 +289,10 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto deferred = [&](const std::size_t index) { return !required[index] && std::find(runtimeDirectories.begin(), runtimeDirectories.end(), images[index].SourcePath.parent_path().lexically_normal()) != runtimeDirectories.end(); }; + const auto appDirectory = [&](const GuestImage& image) { + const auto directory = image.SourcePath.parent_path().lexically_relative(root).generic_string(); + return directory == "." ? std::string("app0") : "app0/" + directory; + }; if (!windows) { for (auto& image : images) { for (std::size_t index = 1; index < image.Symbols.size(); ++index) { @@ -324,7 +329,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size()); Io::AppendString(dynamic.DynStrData, name); }; - for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().lexically_relative(root).generic_string() + "/" + images[index].OutputName); + for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = std::filesystem::absolute(outputPath).parent_path().lexically_normal(); @@ -345,7 +350,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize); } - const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string(); + const auto relativeDirectory = appDirectory(image); const auto destination = outputDirectory / relativeDirectory; const auto target = destination / image.OutputName; if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py index 4ab73d8af..b333399ac 100644 --- a/core/relinker/relinker/tests/test_guest_module_dir.py +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -91,6 +91,35 @@ def main(): else: assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes()) + case = work / f"{windows}-root" + case.mkdir() + (case / "root.prx").write_bytes(module_with_symbol(True)) + (case / "readme.txt").write_text("not ELF") + result, output = convert(case, windows, ["--module-dir", "."]) + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = case / "app0" / "root.prx.guest.prx" + assert list((case / "app0").rglob("*.guest.prx")) == [artifact], list(case.rglob("*.guest.prx")) + assert artifact.read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), artifact + assert " root.prx.guest.prx\n" in result.stdout, result.stdout + if windows: + assert "__aps5_guest_initialize" in pe_export_names(artifact.read_bytes()) + else: + assert not needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes()) + if windows and os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + + case = work / f"{windows}-root-needed" + case.mkdir() + (case / "needed.prx").write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, ["--module-dir", "."], executable_with_needed()) + assert result.returncode == 0, (result.stdout, result.stderr) + needed = case / "app0" / "needed.prx.guest.prx" + if windows: + assert "__aps5_guest_initialize" not in pe_export_names(needed.read_bytes()) + else: + assert "$ORIGIN/app0/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes()) + case = work / f"{windows}-plugins" for rejected, message in ((str(plugins), "inside the guest module parent directory"), ("../outside", "inside the guest module parent directory"), diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa4daa4a..ed8834584 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -56,7 +56,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--rpath ` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. Linux guest modules require an absolute path or a path beginning with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. | | `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. Requires the target OS and prepared runtime layout. | | `--sce-module-path ` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. | -| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module the executable or another module needs is initialized at startup, any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`). Repeat for multiple directories. Conflicts with `--skip-sce-module`. | +| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module the executable or another module needs is initialized at startup, any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`). Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | From d70056700837178d30548ddc224c037acb954119 Mon Sep 17 00:00:00 2001 From: Ayan <108534604+excited-raccoon@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:55:39 +0000 Subject: [PATCH 048/318] perf(relinker): walk fallthrough instructions without queueing --- core/relinker/CMakeLists.txt | 14 ++ .../UnusedNidFilter/ControlFlowGraph.cpp | 149 +++++++-------- .../relinker/tests/ControlFlowGraphTests.cpp | 171 ++++++++++++++++++ 3 files changed, 260 insertions(+), 74 deletions(-) create mode 100644 core/relinker/relinker/tests/ControlFlowGraphTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..9653e5a28 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -393,6 +393,20 @@ if(BUILD_TESTING) ) add_test(NAME entry_stub COMMAND entry_stub_tests) + add_test_executable(control_flow_graph_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ControlFlowGraphTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(control_flow_graph_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ) + add_test(NAME control_flow_graph COMMAND control_flow_graph_tests) + set_tests_properties(control_flow_graph PROPERTIES TIMEOUT 20) + add_test_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index e4301940e..18842fd8c 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -74,87 +74,88 @@ void buildGraph( VirtualAddress va = worklist.front(); worklist.pop(); - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) - throw RelinkerException("CFG: jump target outside text segment", va); + while (va >= textVaddr && va - textVaddr < text.size() && !reachable.contains(va)) { + std::size_t bufOff = static_cast(va - textVaddr); + std::size_t available = text.size() - bufOff; + if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); - std::size_t bufOff = static_cast(va - textVaddr); - std::size_t available = text.size() - bufOff; - if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); - - Codegen::DecodedInstructionInfo info; - if (speculative) { - try { + Codegen::DecodedInstructionInfo info; + if (speculative) { + try { + info = decoder.DecodeInstruction(text.data() + bufOff, available); + } catch (const Codegen::CodegenException&) { + break; + } + } else { info = decoder.DecodeInstruction(text.data() + bufOff, available); - } catch (const Codegen::CodegenException&) { - continue; } - } else { - info = decoder.DecodeInstruction(text.data() + bufOff, available); - } - reachable.insert(va); + reachable.insert(va); + visited.erase(va); - VirtualAddress nextVaddr = va + static_cast(info.Length); + VirtualAddress nextVaddr = va + static_cast(info.Length); - if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { - std::int32_t displacement = 0; - std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - enqueue(static_cast(static_cast(nextVaddr) + displacement)); - } + if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { + std::int32_t displacement = 0; + std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); + enqueue(static_cast(static_cast(nextVaddr) + displacement)); + } - if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { - std::int32_t displacement = 0; - std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - const auto base = static_cast(static_cast(nextVaddr) + displacement); - takeAddress(base); - for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); - } + if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { + std::int32_t displacement = 0; + std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); + const auto base = static_cast(static_cast(nextVaddr) + displacement); + takeAddress(base); + for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); + } - switch (info.FlowKind) { - using enum Codegen::ControlFlowKind; - case Sequential: - enqueue(nextVaddr); - break; - case ConditionalBranch: - enqueue(nextVaddr); - if (info.HasBranchTarget) - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - break; - case UnconditionalJump: - if (info.HasRipRelativeDisp) { - std::int32_t disp = 0; - std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); - auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); - if (resolved.has_value()) - enqueue(*resolved); - } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - } - break; - case Call: - if (info.HasRipRelativeDisp) { - std::int32_t disp = 0; - std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); - auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); - if (resolved.has_value()) - enqueue(*resolved); - } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - } - enqueue(nextVaddr); - break; - case IndirectCall: - enqueue(nextVaddr); - break; - case Return: - case IndirectJump: - case Trap: - break; + bool fallThrough = true; + switch (info.FlowKind) { + using enum Codegen::ControlFlowKind; + case Sequential: + break; + case ConditionalBranch: + if (info.HasBranchTarget) + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + break; + case UnconditionalJump: + fallThrough = false; + if (info.HasRipRelativeDisp) { + std::int32_t disp = 0; + std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); + VirtualAddress slotVaddr = static_cast( + static_cast(nextVaddr) + disp + ); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + } + break; + case Call: + if (info.HasRipRelativeDisp) { + std::int32_t disp = 0; + std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); + VirtualAddress slotVaddr = static_cast( + static_cast(nextVaddr) + disp + ); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + } + break; + case IndirectCall: + break; + case Return: + case IndirectJump: + case Trap: + fallThrough = false; + break; + } + if (!fallThrough) break; + va = nextVaddr; } } } diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp new file mode 100644 index 000000000..5870e3c5e --- /dev/null +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Relinker::UnusedNidFilter; +using Relinker::VirtualAddress; +using Bytes = std::vector; +constexpr VirtualAddress TextAddress = 0xec00001000; + +class Pointers : public IRelativeRelocationIndex { +public: + std::map Slots; + + std::optional TargetOfSlot(VirtualAddress slot) const override { + const auto position = Slots.find(slot); + if (position == Slots.end()) return std::nullopt; + return position->second; + } + + std::vector Targets() const override { + std::vector targets; + for (const auto& [slot, target] : Slots) targets.push_back(target); + return targets; + } +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void RequireOffsets(const IControlFlowGraph& graph, std::initializer_list offsets) { + std::unordered_set expected; + for (const auto offset : offsets) expected.insert(TextAddress + offset); + Require(graph.ReachableVaddrs() == expected, "Unexpected reachable instruction set"); + for (const auto address : expected) Require(graph.IsReachable(address), "Missing reachable instruction"); + Require(!graph.IsReachable(TextAddress - 1), "Address before code is reachable"); +} + +void SetDisplacement(Bytes& text, std::size_t offset, std::size_t next, VirtualAddress target) { + const auto displacement = static_cast(target - (TextAddress + next)); + std::memcpy(text.data() + offset, &displacement, sizeof(displacement)); +} + +void CheckFallthrough() { + const Pointers pointers; + const Bytes text{0x90, 0x48, 0x89, 0xc0, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 1, 4}); + const auto end = BuildControlFlowGraph(Bytes{0x90, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*end, {0, 1}); + const auto empty = BuildControlFlowGraph(Bytes{}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*empty, {}); + const auto outside = BuildControlFlowGraph(text, TextAddress, TextAddress - 1, {TextAddress + text.size()}, pointers); + RequireOffsets(*outside, {}); +} + +void CheckBranches() { + const Pointers pointers; + const Bytes text{0x75, 0x03, 0x90, 0xeb, 0x03, 0x90, 0xeb, 0xfa, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress, TextAddress + 2}, pointers); + RequireOffsets(*graph, {0, 2, 3, 5, 6, 8}); + const auto self = BuildControlFlowGraph(Bytes{0xeb, 0xfe, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*self, {0}); + const auto outside = BuildControlFlowGraph(Bytes{0xeb, 0x7f, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*outside, {0}); + const auto trap = BuildControlFlowGraph(Bytes{0x0f, 0x0b, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*trap, {0}); +} + +void CheckCalls() { + const Pointers pointers; + const Bytes text{0xe8, 0x03, 0, 0, 0, 0x90, 0xc3, 0x90, 0xe8, 0xf3, 0xff, 0xff, 0xff, 0xc3}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 5, 6, 8, 13}); + const auto indirect = BuildControlFlowGraph(Bytes{0xff, 0xd0, 0x90, 0xff, 0xe0, 0xc3}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*indirect, {0, 2, 3}); +} + +void CheckRelocatedTransfers() { + constexpr auto slot = TextAddress + 0x1000; + Bytes text{0xff, 0x15, 0, 0, 0, 0, 0x90, 0xc3, 0xff, 0x25, 0, 0, 0, 0, 0x90, 0x90, 0xc3}; + SetDisplacement(text, 2, 6, slot); + SetDisplacement(text, 10, 14, slot + 8); + Pointers pointers; + pointers.Slots = {{slot, TextAddress + 8}, {slot + 8, TextAddress + 16}}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 6, 7, 8, 16}); + const Pointers missing; + const auto unresolved = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 8}, missing); + RequireOffsets(*unresolved, {0, 6, 7, 8}); +} + +void CheckCodeAddresses() { + Bytes text(17, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 16); + text[7] = text[16] = 0xc3; + const Pointers pointers; + const auto ordinary = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*ordinary, {0, 7}); + const auto addresses = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, true); + RequireOffsets(*addresses, {0, 7, 16}); +} + +void CheckSpeculativeRoots() { + constexpr auto table = TextAddress + 0x1000; + Bytes text(32, 0xc3); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, table); + text[16] = 0x90; + text[31] = 0x0f; + Bytes entries(12); + const std::int32_t targets[]{static_cast(TextAddress + 16 - table), + static_cast(TextAddress + 31 - table), 0}; + std::memcpy(entries.data(), targets, sizeof(targets)); + Pointers pointers; + pointers.Slots = {{table + 16, TextAddress + 24}, {table + 24, TextAddress + 31}}; + const std::vector data{{table, entries}}; + const auto graph = BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, data); + RequireOffsets(*graph, {0, 7, 16, 17, 24}); + bool threw = false; + try { + BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 31}, pointers, data); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "Malformed mandatory root was treated as speculative"); +} + +void CheckBorrowedReachability() { + const Pointers pointers; + const Bytes text{0x90, 0x90, 0xc3, 0x90, 0xc3}; + std::unordered_set reachable{TextAddress + 1}; + const auto first = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress, {}, pointers, false, reachable); + RequireOffsets(*first, {0, 1}); + Require(&first->ReachableVaddrs() == &reachable, "Borrowed graph copied the result set"); + const auto second = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress + 3, + {TextAddress + 3, TextAddress + 4}, pointers, false, reachable); + RequireOffsets(*second, {0, 1, 3, 4}); + RequireOffsets(*first, {0, 1, 3, 4}); +} + +} + +int main() { + try { + CheckFallthrough(); + CheckBranches(); + CheckCalls(); + CheckRelocatedTransfers(); + CheckCodeAddresses(); + CheckSpeculativeRoots(); + CheckBorrowedReachability(); + std::cout << "Control-flow graph tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "control_flow_graph_tests: " << error.what() << '\n'; + return 1; + } +} From 36d37a062a7a720f17df8edfcb509659e7e9430e Mon Sep 17 00:00:00 2001 From: Lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:56:47 +0200 Subject: [PATCH 049/318] fix(libkernel): limit UTC-to-local DST output to 32 bits --- core/libs/prx/libkernel/Time/Time.cpp | 2 +- core/libs/tests/GuestFreebsdClocks.cpp | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 581e81bb5..9e7336ea7 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -581,7 +581,7 @@ int APS5_VABI sceKernelConvertLocaltimeToUtc(int64_t local_time, int64_t reserve return 0; } -int APS5_VABI sceKernelConvertUtcToLocaltime(int64_t utc_time, int64_t* local_time, KernelTimesec* st, uint64_t* dst_sec) { +int APS5_VABI sceKernelConvertUtcToLocaltime(int64_t utc_time, int64_t* local_time, KernelTimesec* st, int32_t* dst_sec) { if (local_time != nullptr) *local_time = utc_time; if (st != nullptr) *st = {utc_time, 0u, 0u}; if (dst_sec != nullptr) *dst_sec = 0; diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index be5f30434..e5ba4c678 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -12,7 +12,7 @@ int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); int APS5_VABI sceKernelConvertLocaltimeToUtc(std::int64_t, std::int64_t, std::int64_t*, KernelTimesec*, std::int32_t*); -int APS5_VABI sceKernelConvertUtcToLocaltime(std::int64_t, std::int64_t*, KernelTimesec*, std::uint64_t*); +int APS5_VABI sceKernelConvertUtcToLocaltime(std::int64_t, std::int64_t*, KernelTimesec*, std::int32_t*); } static constexpr int SCE_OK = 0; @@ -147,6 +147,11 @@ static void CalendarConversionsInitializeTheirState() { KernelTimesec state; std::uint64_t after; }; + struct GuardedDst { + std::int32_t seconds; + std::uint32_t after; + }; + static_assert(sizeof(GuardedDst) == 8); for (const auto seconds : std::array{-86400, 0, 1767225600, 2147483648}) { GuardedState local{sentinel, {-1, 0xa5a5a5a5u, 0xa5a5a5a5u}, sentinel}; std::int64_t utc = -1; @@ -157,9 +162,10 @@ static void CalendarConversionsInitializeTheirState() { Require(local.before == sentinel && local.after == sentinel); GuardedState universal{sentinel, {-1, 0xa5a5a5a5u, 0xa5a5a5a5u}, sentinel}; std::int64_t converted = -1; - std::uint64_t universalDst = sentinel; - Require(sceKernelConvertUtcToLocaltime(utc, &converted, &universal.state, &universalDst) == SCE_OK); - Require(converted == seconds && universalDst == 0); + GuardedDst universalDst{-1, 0xa5a5a5a5u}; + Require(sceKernelConvertUtcToLocaltime(utc, &converted, &universal.state, &universalDst.seconds) == SCE_OK); + Require(converted == seconds && universalDst.seconds == 0); + Require(universalDst.after == 0xa5a5a5a5u); Require(universal.state.t == seconds && universal.state.west_sec == 0 && universal.state.dst_sec == 0); Require(universal.before == sentinel && universal.after == sentinel); } From 07af520c1c19a67ce19bffff6cc8f6807db38af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 19:58:11 +0200 Subject: [PATCH 050/318] docs(technical-debt): move the vieBRwlh1Lw entry off the end of Functional Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3827dbf51..53e47c93f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -322,6 +322,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSharePlayInitialize](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the library keeps no state, so the heap is not used. A null heap is accepted: PPSA11386 passes a null heap and 0x1800 bytes and stops on any non-zero result. The error for a zero size is unknown, so it throws - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` - [sceAgcDriverRegisterMultipleResources](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. - [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown - [Shader header copies](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header except `user_data`; the registered header's metadata is used. PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null. Whether the console reads the copy's `user_data` is unknown @@ -597,4 +598,3 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` From c61f07409f8c83307bc475447a4e9c5e7c98445f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 19:58:44 +0200 Subject: [PATCH 051/318] test(shader): cover clamp alone and inf/NaN/fraction rows in v_cvt_i16_f16 output modifiers Co-Authored-By: Claude --- .../tests/execution/VCvtI16F16OutputModifiers.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp index b4517eaac..d7a66c2b4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp @@ -19,7 +19,7 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 16; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 4; -constexpr std::uint32_t RowCount = 12; +constexpr std::uint32_t RowCount = 16; alignas(256) std::array Input{}; alignas(256) std::array Output{}; @@ -39,6 +39,7 @@ constexpr Variant Variants[] = { {"mul:2", 0, 1}, {"mul:4", 0, 2}, {"div:2", 0, 3}, + {"clamp", 1, 0}, {"clamp mul:2", 1, 1}, }; @@ -55,12 +56,17 @@ constexpr std::uint32_t Rows[RowCount] = { 0x00007e00u, 0xbeef3c00u, 0xbeefc100u, + 0x00007c00u, + 0x0000fc00u, + 0x00007c01u, + 0x00003e00u, }; constexpr std::uint32_t Expected[RowCount] = { 0x5a5a0001u, 0x5a5affffu, 0x5a5afffeu, 0x5a5a0003u, 0x5a5a0400u, 0x5a5a0064u, 0x5a5aff9cu, 0x5a5a7fffu, 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5afffeu, + 0x5a5a7fffu, 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { From 5cd25dedbd2e673d210b6567a078b5f6f7590d28 Mon Sep 17 00:00:00 2001 From: Lexroid Date: Sun, 11 Oct 2026 21:06:04 +0300 Subject: [PATCH 052/318] fix(videoout): accept a buffer attribute change while a flip is pending sceVideoOutSubmitChangeBufferAttribute2 threw when a buffer of the group had a flip pending, and Marvel's Wolverine makes that call with flips in the queue. A flip now takes its buffer group when it is reserved, in submission order, so flips submitted before the change keep the attributes their buffers were rendered with and later flips use the new ones. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceVideoOut/include/VideoOutDriver.hpp | 1 + core/libs/prx/libSceVideoOut/src/Buffer.cpp | 1 - core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp | 6 +++++- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 10 +++++++++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 0bab9d5e5..11fa5eb89 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -173,6 +173,7 @@ struct FlipRequest final : AgcDriver::IFlipRequest, std::enable_shared_from_this uint32_t height = 0; VideoOutBuffer buffer; BufferAttributeGroup group; + bool groupCaptured = false; bool reserved = false; bool unregistered = false; bool ready = false; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 481c92c2f..25589edd2 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -127,7 +127,6 @@ int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, group.attribute = *attribute; for (int i = 0; i < VIDEO_OUT_BUFFER_NUM_MAX; ++i) { if (cfg->buffers[i].groupIndex != setIndex) continue; - if (cfg->bufferPending[i] != 0) throw std::runtime_error("VideoOut: buffer attributes are in use by a pending flip"); const auto display = DescribeVideoOutBuffer(cfg->buffers[i], group); AgcDriverCheckGuestMemory_nid_postfix(reinterpret_cast(display.address), AgcDriverDisplayBufferSize_nid_postfix(display), 65536); } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 5e7b5c1a3..fbe9da37b 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -84,7 +84,7 @@ void resolveFlipBuffer(FlipRequest& request, const VideoOutConfig& cfg) { return; } require(request.buffer.groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX, "invalid buffer group"); - request.group = cfg.groups[request.buffer.groupIndex]; + if (!request.groupCaptured) request.group = cfg.groups[request.buffer.groupIndex]; require(request.group.occupied, "buffer group is not registered"); require(request.buffer.dataAddress != 0, "null registered buffer address"); static_cast(DescribeVideoOutBuffer(request.buffer, request.group)); @@ -152,6 +152,10 @@ public: ++queue->reservations; ++cfg->flipStatus.flipPendingNum; if (info.index >= 0) ++cfg->bufferPending[info.index]; + if (info.index >= 0 && cfg->buffers[info.index].Occupied() && cfg->buffers[info.index].groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX && cfg->groups[cfg->buffers[info.index].groupIndex].occupied) { + request->group = cfg->groups[cfg->buffers[info.index].groupIndex]; + request->groupCaptured = true; + } request->reserved = true; return request; } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 607870626..bfc3bd07d 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -86,8 +86,16 @@ void testLifetime(bool reopen) { std::lock_guard lock(cfg->mutex); check(cfg->flipStatus.flipPendingNum == 1 && cfg->flipStatus.count == 0, "reservation status is wrong"); } + auto smaller = attribute; + smaller.width = 32; + smaller.height = 32; + check(sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &smaller, nullptr) == 0, "attributes of a buffer with a pending flip could not be changed"); + { + std::lock_guard lock(cfg->mutex); + check(cfg->groups[0].attribute.width == 32 && cfg->groups[0].attribute.height == 32 && cfg->flipStatus.flipPendingNum == 1, "the attribute change did not reach later flips or dropped the pending one"); + } attribute.dcc_control = 1; - check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr); }).find("pending flip") != std::string::npos, "pending attributes were changed"); + check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr); }).find("DCC") != std::string::npos, "an invalid attribute change was accepted"); check(expectFailure([&] { sceVideoOutUnregisterBuffers(handle, 0); }).find("pending flip") != std::string::npos, "pending buffer was unregistered"); check(sceVideoOutWaitVblank(handle) == 0 && sceVideoOutWaitVblank(handle) == 0, "vblank stalled behind the GPU queue"); std::shared_ptr replacement; From 565e8e9c4a5dcccc416b06967c5f7df72fd0508e Mon Sep 17 00:00:00 2001 From: Tyler Westerberg Date: Sun, 11 Oct 2026 14:11:39 -0400 Subject: [PATCH 053/318] feat(libSceJson2): implement Object::find, Object::iterator::operator-> and String::compare Goat Simulator 3 (PPSA04159) imports sce::Json::Object::find(String const&) const (ehN1Biw3tw8), Object::iterator::operator->() const (EnesV4gXDtE) and String::compare(char const*) const (Yfw63FVJgtU); without them the Windows entry stub stops before the game starts with "unresolved ELF import ehN1Biw3tw8". find returns an iterator to the first pair whose key equals the argument, or end(), using the same key match as Object::operator[]. operator-> returns the pair like operator*. compare orders the string against a C string as std::string::compare does; a null argument throws. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceJson2/Export.cpp | 10 ++++++ core/libs/tests/GuestJson.cpp | 54 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 672d857ee..11afc3de1 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -527,6 +528,10 @@ const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String* self) { return bool APS5_VABI _ZNK3sce4Json6String5emptyEv(const String* self) { return self->text->empty(); } std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String* self) { return self->text->size(); } bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String* self, const char* text) { return text != nullptr && *self->text == text; } +int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String* self, const char* text) { + if (text == nullptr) throw std::invalid_argument("sce::Json::String::compare: null text"); + return self->text->compare(text); +} void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array* self) { Destroy(*self); } @@ -572,6 +577,11 @@ ObjectIterator* APS5_VABI _ZNK3sce4Json6Object3endEv(ObjectIterator* result, con void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(ObjectIterator* self) { (void)self; } ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(ObjectIterator* self) { ++self->it; return self; } Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const ObjectIterator* self) { return &*self->it; } +Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const ObjectIterator* self) { return &*self->it; } +ObjectIterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(ObjectIterator* result, const Object* self, const String* key) { + result->it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return *pair.key.text == *key->text; }); + return result; +} bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it != other->it; } bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object* self) { return self->items->empty(); } std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object* self) { return self->items->size(); } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 8150bc249..90f9064a8 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -53,6 +53,9 @@ Iterator* APS5_VABI _ZNK3sce4Json6Object3endEv(Iterator*, const Object*); void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(Iterator*); Iterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(Iterator*); Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const Iterator*); +Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const Iterator*); +Iterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(Iterator*, const Object*, const String*); +int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String*, const char*); bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const Iterator*, const Iterator*); void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); void APS5_VABI _ZN3sce4Json5ValueC1Eb(Value*, bool); @@ -271,6 +274,56 @@ static void ObjectsAndArrays() { _ZN3sce4Json5ArrayD1Ev(&array); } +static void ObjectFind() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + String alpha{}, beta{}, gamma{}; + _ZN3sce4Json6StringC1EPKc(&alpha, "alpha"); + _ZN3sce4Json6StringC1EPKc(&beta, "beta"); + _ZN3sce4Json6StringC1EPKc(&gamma, "gamma"); + Iterator found{}, end{}; + _ZNK3sce4Json6Object3endEv(&end, &object); + _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &alpha); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + _ZN3sce4Json6Object8iteratorD1Ev(&found); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + + Require(_ZN3sce4Json5Value3setEl(_ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &alpha), 1) == 0); + Value* second = _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &beta); + Require(_ZN3sce4Json5Value3setEPKc(second, "b") == 0); + _ZNK3sce4Json6Object3endEv(&end, &object); + Iterator* returned = _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &beta); + Require(returned == &found); + Require(_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + const Pair* pair = _ZNK3sce4Json6Object8iteratorptEv(&found); + Require(pair == _ZNK3sce4Json6Object8iteratordeEv(&found)); + Require(&pair->value == second); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&pair->key), "beta") == 0); + _ZN3sce4Json6Object8iteratorppEv(&found); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &gamma); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + _ZN3sce4Json6Object8iteratorD1Ev(&found); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "beta") == 0); + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "betb") < 0); + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "bet") > 0); + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "alpha") > 0); + bool threw = false; + try { + static_cast(_ZNK3sce4Json6String7compareEPKc(&beta, nullptr)); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + _ZN3sce4Json6StringD1Ev(&alpha); + _ZN3sce4Json6StringD1Ev(&beta); + _ZN3sce4Json6StringD1Ev(&gamma); + _ZN3sce4Json6ObjectD1Ev(&object); +} + static void NullAccess(SetNullAccessCallback setCallback) { callbackCalls = 0; lastRequested = -1; @@ -402,6 +455,7 @@ int main() { ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); + ObjectFind(); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallBackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); ValueAccess(); From eedf7a267124dfa46c9771cda3bb08198ca6ca5b Mon Sep 17 00:00:00 2001 From: Chad Date: Sun, 11 Oct 2026 13:23:50 -0500 Subject: [PATCH 054/318] feat(savedata): implement sceSaveDataConvert PPSA28416 calls it on its existing save at boot (after sceSaveDataDirNameSearch finds it) and aborted on the missing implementation. The call takes one parameter block: user id at +0, the directory name pointer at +8. Host saves have no size limit, so the conversion completes at once: an existing save returns OK, a missing one NOT_FOUND, an invalid name PARAMETER. The title polls sceSaveDataGetEventResult once afterwards and continues on 'no event'. --- core/libs/SceTypes.hpp | 7 ++ .../prx/libSceSaveData.native/CMakeLists.txt | 9 +++ .../libs/prx/libSceSaveData.native/Export.cpp | 25 ++++++- core/libs/tests/GuestSaveDataConvert.cpp | 73 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 5 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestSaveDataConvert.cpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index bd63ff766..b288e6227 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1563,6 +1563,13 @@ struct SaveDataBackup { std::uint8_t reserved[32]; }; +struct SaveDataConvert { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataDirName* dir_name; + std::uint8_t unknown[48]; +}; + struct AppContentInitParam { char reserved[32]; }; struct AppContentBootParam { diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 3b596955e..6b03a2c86 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -22,6 +22,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_savedata_convert_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataConvert.cpp) + target_include_directories(guest_savedata_convert_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_savedata_convert_tests PRIVATE libSceSaveData.native libc) + add_test(NAME guest_savedata_convert COMMAND guest_savedata_convert_tests) + if(WIN32) + set_tests_properties(guest_savedata_convert PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_savedata_mount_status_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataMountStatus.cpp) target_include_directories(guest_savedata_mount_status_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_savedata_mount_status_tests PRIVATE libSceSaveData.native libc) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 4f96392ed..aeaca60c5 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -740,9 +740,28 @@ int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, return rc; } -int APS5_VABI sceSaveDataConvert() { - NotImplemented_nid_no_patch(__func__); - return 0; +static int convert(const SaveDataConvert* param) { + if (param == nullptr || param->dir_name == nullptr) { + throw std::runtime_error("sceSaveDataConvert: null argument"); + } + const auto* nameEnd = static_cast(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); + if (nameEnd == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + const std::string dirName(param->dir_name->data, static_cast(nameEnd - param->dir_name->data)); + if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!std::filesystem::is_directory(save_root() + "/" + dirName)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataConvert(const SaveDataConvert* param) { + const int rc = convert(param); + SAVEDATA_TRACE("convert user=%d dir=%s -> 0x%08x", param != nullptr ? param->user_id : -1, param != nullptr && param->dir_name != nullptr ? param->dir_name->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataGetConvertProgress() { diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp new file mode 100644 index 000000000..8df9b84da --- /dev/null +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSaveDataConvert(const SaveDataConvert*); +int APS5_VABI sceSaveDataInitialize3(const void*); +} + +namespace { + +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotFound = -2137063416; + +int failures = 0; + +void Check(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "savedata convert check failed: %s\n", what.c_str()); + ++failures; +} + +int Convert(const char* data, std::size_t size) { + SceSaveDataDirName name{}; + std::memcpy(name.data, data, size); + SaveDataConvert convert{}; + convert.user_id = 0x10000000; + convert.dir_name = &name; + return sceSaveDataConvert(&convert); +} + +std::vector Read(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-convert-" + std::to_string(std::random_device{}())); + const auto work = root / "work"; + const auto kept = work / "_sd" / "kept" / "data.bin"; + std::filesystem::create_directories(kept.parent_path()); + std::ofstream(kept) << "data"; + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(work); + + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + Check(Convert("kept", sizeof("kept")) == 0, "an existing save converts"); + Check(Read(kept) == std::vector{'d', 'a', 't', 'a'}, "the converted save keeps its files"); + Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); + for (const char* invalid : {"../kept", "", ".", "..", "a\\b", "c:d"}) { + Check(Convert(invalid, std::strlen(invalid) + 1) == SaveDataErrorParameter, std::string("\"") + invalid + "\" is rejected"); + } + char unterminated[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminated, 'a', sizeof(unterminated)); + Check(Convert(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); + + std::filesystem::current_path(previous); + std::error_code error; + std::filesystem::remove_all(root, error); + if (failures != 0) return 1; + std::printf("savedata convert tests passed\n"); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..aaffeaec6 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -125,7 +125,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title. The 64-bit writing attribute is read as the PS4 11.00 `libSceFont.sprx` reads it: low byte `0xFA`, embedding level in bits 8-15 (0 to 126), bit 30 and bit 31 marking space before and after the run; the PS5 library was not measured, and the names of the two bits are ours. PPSA01325 builds its attribute the same way (`0xFA | level << 8`, bits 30 and 31) and passes a null metrics pointer when its run has no metrics object. Attribute 0 is accepted and treated as `0xFA` with level 0 and no spacing bits, as before this reading; the PS4 library rejects it and what the PS5 library does with it is unknown. Any other attribute, a level above 126 and a null orderer throw: the PS4 library returns `0x80460002` for them, not confirmed for the PS5 - [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits -- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataConvert, sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures +- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures - [sceSaveDataCancel](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached From 95988c9c3443f27d926722dcf5143a99e9e84fe0 Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:27:58 -0400 Subject: [PATCH 055/318] fix(shader): flush f16 denormals in unary, frexp and conversion ops per the float mode The f16 unary ops, v_frexp_mant_f16, v_frexp_exp_i16_f16, the norm conversions and v_cvt_f32_f16 read denormal halves as they are and rounded denormal results as they are, whatever the denormal field of FLOAT_MODE says. On RDNA2 a clear bit 0 flushes a denormal half input to its signed zero and a clear bit 1 flushes a denormal half result. One shared rule now drives all of them, and v_ldexp_f16 uses it for its input too. Co-Authored-By: Claude Sonnet 5.5 --- .../tests/execution/F16DenormalFlush.cpp | 353 ++++++++++++++++++ .../Translation/TranslationContext.hpp | 4 + .../Translation/src/ConvertInstructions.cpp | 2 +- .../Translation/src/FloatInstructions.cpp | 15 +- .../src/TranslationContext/SubwordPacking.cpp | 21 ++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 386 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F16DenormalFlush.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F16DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16DenormalFlush.cpp new file mode 100644 index 000000000..60e0f3fac --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16DenormalFlush.cpp @@ -0,0 +1,353 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 1; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct alignas(256) Shader { + std::array words; +}; + +struct Vector { + std::uint32_t half; + std::uint32_t expected[4]; +}; + +struct Case { + const char* name; + std::uint32_t word; + std::span rows; +}; + +constexpr Vector CeilRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a3c00u, 0x5a5a0000u, 0x5a5a3c00u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a3c00u, 0x5a5a0000u, 0x5a5a3c00u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a3c00u, 0x5a5a0000u, 0x5a5a3c00u}}, + {0x8001u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x8200u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x0400u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8400u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0x3800u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, +}; + +constexpr Vector CvtF32Rows[] = { + {0x0001u, {0x00000000u, 0x33800000u, 0x00000000u, 0x33800000u}}, + {0x0200u, {0x00000000u, 0x38000000u, 0x00000000u, 0x38000000u}}, + {0x03ffu, {0x00000000u, 0x387fc000u, 0x00000000u, 0x387fc000u}}, + {0x8001u, {0x80000000u, 0xb3800000u, 0x80000000u, 0xb3800000u}}, + {0x8200u, {0x80000000u, 0xb8000000u, 0x80000000u, 0xb8000000u}}, + {0x83ffu, {0x80000000u, 0xb87fc000u, 0x80000000u, 0xb87fc000u}}, + {0x0400u, {0x38800000u, 0x38800000u, 0x38800000u, 0x38800000u}}, + {0x8400u, {0xb8800000u, 0xb8800000u, 0xb8800000u, 0xb8800000u}}, + {0x0000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3c00u, {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u}}, + {0xbc00u, {0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u}}, + {0x3800u, {0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u}}, +}; + +constexpr Vector CvtNormI16Rows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0001u, 0x5a5a0000u, 0x5a5a0001u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0002u, 0x5a5a0000u, 0x5a5a0002u}}, + {0x8001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8200u, {0x5a5a0000u, 0x5a5affffu, 0x5a5a0000u, 0x5a5affffu}}, + {0x83ffu, {0x5a5a0000u, 0x5a5afffeu, 0x5a5a0000u, 0x5a5afffeu}}, + {0x0400u, {0x5a5a0002u, 0x5a5a0002u, 0x5a5a0002u, 0x5a5a0002u}}, + {0x8400u, {0x5a5afffeu, 0x5a5afffeu, 0x5a5afffeu, 0x5a5afffeu}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a7fffu, 0x5a5a7fffu, 0x5a5a7fffu, 0x5a5a7fffu}}, + {0xbc00u, {0x5a5a8001u, 0x5a5a8001u, 0x5a5a8001u, 0x5a5a8001u}}, + {0x3800u, {0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u}}, + {0xb800u, {0x5a5ac000u, 0x5a5ac000u, 0x5a5ac000u, 0x5a5ac000u}}, +}; + +constexpr Vector CvtNormU16Rows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0002u, 0x5a5a0000u, 0x5a5a0002u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0004u, 0x5a5a0000u, 0x5a5a0004u}}, + {0x8001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8200u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x83ffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0400u, {0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u}}, + {0x8400u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5affffu, 0x5a5affffu, 0x5a5affffu, 0x5a5affffu}}, + {0xbc00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3800u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0xb800u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, +}; + +constexpr Vector ExpRows[] = { + {0x0001u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x0200u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x03ffu, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8001u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8200u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x83ffu, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x0400u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8400u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x0000u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x3c00u, {0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u}}, + {0xbc00u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0xcb01u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a03fau, 0x5a5a03fau}}, + {0xcca0u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a002du, 0x5a5a002du}}, + {0xce3cu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5a0001u}}, + {0xc800u, {0x5a5a1c00u, 0x5a5a1c00u, 0x5a5a1c00u, 0x5a5a1c00u}}, + {0xc400u, {0x5a5a2c00u, 0x5a5a2c00u, 0x5a5a2c00u, 0x5a5a2c00u}}, +}; + +constexpr Vector FloorRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8001u, {0x5a5a8000u, 0x5a5abc00u, 0x5a5a8000u, 0x5a5abc00u}}, + {0x8200u, {0x5a5a8000u, 0x5a5abc00u, 0x5a5a8000u, 0x5a5abc00u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5abc00u, 0x5a5a8000u, 0x5a5abc00u}}, + {0x0400u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8400u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0xb800u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, +}; + +constexpr Vector FractRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0001u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0200u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu}}, + {0x8001u, {0x5a5a0000u, 0x5a5a3bffu, 0x5a5a0000u, 0x5a5a3bffu}}, + {0x8200u, {0x5a5a0000u, 0x5a5a3bffu, 0x5a5a0000u, 0x5a5a3bffu}}, + {0x83ffu, {0x5a5a0000u, 0x5a5a3bffu, 0x5a5a0000u, 0x5a5a3bffu}}, + {0x0400u, {0x5a5a0400u, 0x5a5a0400u, 0x5a5a0400u, 0x5a5a0400u}}, + {0x8400u, {0x5a5a3bffu, 0x5a5a3bffu, 0x5a5a3bffu, 0x5a5a3bffu}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xbc00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xb800u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0x4900u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, +}; + +constexpr Vector FrexpExpRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5affe9u, 0x5a5a0000u, 0x5a5affe9u}}, + {0x0200u, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x8001u, {0x5a5a0000u, 0x5a5affe9u, 0x5a5a0000u, 0x5a5affe9u}}, + {0x8200u, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x83ffu, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x0400u, {0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u}}, + {0x8400u, {0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u}}, + {0xbc00u, {0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u}}, + {0x4900u, {0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u}}, +}; + +constexpr Vector FrexpMantRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a3800u, 0x5a5a0000u, 0x5a5a3800u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a3800u, 0x5a5a0000u, 0x5a5a3800u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a3bfeu, 0x5a5a0000u, 0x5a5a3bfeu}}, + {0x8001u, {0x5a5a8000u, 0x5a5ab800u, 0x5a5a8000u, 0x5a5ab800u}}, + {0x8200u, {0x5a5a8000u, 0x5a5ab800u, 0x5a5a8000u, 0x5a5ab800u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5abbfeu, 0x5a5a8000u, 0x5a5abbfeu}}, + {0x0400u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0x8400u, {0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0xbc00u, {0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u}}, + {0x4900u, {0x5a5a3900u, 0x5a5a3900u, 0x5a5a3900u, 0x5a5a3900u}}, +}; + +constexpr Vector LogRows[] = { + {0x0001u, {0x5a5afc00u, 0x5a5ace00u, 0x5a5afc00u, 0x5a5ace00u}}, + {0x0200u, {0x5a5afc00u, 0x5a5acb80u, 0x5a5afc00u, 0x5a5acb80u}}, + {0x03ffu, {0x5a5afc00u, 0x5a5acb00u, 0x5a5afc00u, 0x5a5acb00u}}, + {0x8001u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x8200u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x83ffu, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x0400u, {0x5a5acb00u, 0x5a5acb00u, 0x5a5acb00u, 0x5a5acb00u}}, + {0x8400u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x0000u, {0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u}}, + {0x3c00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xbc00u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x4900u, {0x5a5a42a5u, 0x5a5a42a5u, 0x5a5a42a5u, 0x5a5a42a5u}}, + {0x0801u, {0x5a5aca80u, 0x5a5aca80u, 0x5a5aca80u, 0x5a5aca80u}}, +}; + +constexpr Vector RcpRows[] = { + {0x0001u, {0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u}}, + {0x0200u, {0x5a5a7c00u, 0x5a5a7800u, 0x5a5a7c00u, 0x5a5a7800u}}, + {0x03ffu, {0x5a5a7c00u, 0x5a5a7401u, 0x5a5a7c00u, 0x5a5a7401u}}, + {0x8001u, {0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u}}, + {0x8200u, {0x5a5afc00u, 0x5a5af800u, 0x5a5afc00u, 0x5a5af800u}}, + {0x83ffu, {0x5a5afc00u, 0x5a5af401u, 0x5a5afc00u, 0x5a5af401u}}, + {0x0400u, {0x5a5a7400u, 0x5a5a7400u, 0x5a5a7400u, 0x5a5a7400u}}, + {0x8400u, {0x5a5af400u, 0x5a5af400u, 0x5a5af400u, 0x5a5af400u}}, + {0x0000u, {0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0x7bffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0100u, 0x5a5a0100u}}, + {0x7401u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu, 0x5a5a03ffu}}, + {0xfbffu, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8100u, 0x5a5a8100u}}, + {0xf404u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a83fcu, 0x5a5a83fcu}}, + {0x7800u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0200u, 0x5a5a0200u}}, +}; + +constexpr Vector RsqRows[] = { + {0x0001u, {0x5a5a7c00u, 0x5a5a6c00u, 0x5a5a7c00u, 0x5a5a6c00u}}, + {0x0200u, {0x5a5a7c00u, 0x5a5a59a8u, 0x5a5a7c00u, 0x5a5a59a8u}}, + {0x03ffu, {0x5a5a7c00u, 0x5a5a5801u, 0x5a5a7c00u, 0x5a5a5801u}}, + {0x8001u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x8200u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x83ffu, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x0400u, {0x5a5a5800u, 0x5a5a5800u, 0x5a5a5800u, 0x5a5a5800u}}, + {0x8400u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x0000u, {0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x4400u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, +}; + +constexpr Vector SinRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0006u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0a48u, 0x5a5a0000u, 0x5a5a0a48u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0e47u, 0x5a5a0000u, 0x5a5a0e47u}}, + {0x8001u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8006u}}, + {0x8200u, {0x5a5a8000u, 0x5a5a8a48u, 0x5a5a8000u, 0x5a5a8a48u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5a8e47u, 0x5a5a8000u, 0x5a5a8e47u}}, + {0x0400u, {0x5a5a0e48u, 0x5a5a0e48u, 0x5a5a0e48u, 0x5a5a0e48u}}, + {0x8400u, {0x5a5a8e48u, 0x5a5a8e48u, 0x5a5a8e48u, 0x5a5a8e48u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xbc00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x00a0u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03edu}}, + {0x80a0u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a83edu}}, + {0x3800u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, +}; + +constexpr Vector SqrtRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0c00u, 0x5a5a0000u, 0x5a5a0c00u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a1da8u, 0x5a5a0000u, 0x5a5a1da8u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a1fffu, 0x5a5a0000u, 0x5a5a1fffu}}, + {0x8001u, {0x5a5a8000u, 0x5a5afe00u, 0x5a5a8000u, 0x5a5afe00u}}, + {0x8200u, {0x5a5a8000u, 0x5a5afe00u, 0x5a5a8000u, 0x5a5afe00u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5afe00u, 0x5a5a8000u, 0x5a5afe00u}}, + {0x0400u, {0x5a5a2000u, 0x5a5a2000u, 0x5a5a2000u, 0x5a5a2000u}}, + {0x8400u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x4400u, {0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u}}, +}; + +constexpr Case Cases[] = { + {"v_ceil_f16", 0x7e14b904u, CeilRows}, + {"v_cvt_f32_f16", 0x7e141704u, CvtF32Rows}, + {"v_cvt_norm_i16_f16", 0x7e14c704u, CvtNormI16Rows}, + {"v_cvt_norm_u16_f16", 0x7e14c904u, CvtNormU16Rows}, + {"v_exp_f16", 0x7e14b104u, ExpRows}, + {"v_floor_f16", 0x7e14b704u, FloorRows}, + {"v_fract_f16", 0x7e14bf04u, FractRows}, + {"v_frexp_exp_i16_f16", 0x7e14b504u, FrexpExpRows}, + {"v_frexp_mant_f16", 0x7e14b304u, FrexpMantRows}, + {"v_log_f16", 0x7e14af04u, LogRows}, + {"v_rcp_f16", 0x7e14a904u, RcpRows}, + {"v_rsq_f16", 0x7e14ad04u, RsqRows}, + {"v_sin_f16", 0x7e14c104u, SinRows}, + {"v_sqrt_f16", 0x7e14ab04u, SqrtRows}, +}; + +constexpr std::uint32_t CaseCount = std::size(Cases); +std::array Shaders{}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Case& item, const Shader& shader, std::uint32_t denorm16, bool ieee) { + Input.fill(0u); + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + Input[row * Inputs] = item.rows[row].half; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(shader.words); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{(3u << 4u) | (denorm16 << 6u), true, ieee, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Case& item, std::uint32_t denorm16, bool ieee) { + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + const auto actual = Output[row]; + const auto expected = item.rows[row].expected[denorm16]; + Require(actual == expected, std::string(item.name) + " denorm16 " + std::to_string(denorm16) + (ieee ? " ieee" : " no ieee") + " input " + + Hex(item.rows[row].half) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < CaseCount; ++index) { + Shaders[index].words = {0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1402ffu, 0x5a5a5a5au, Cases[index].word, + 0xe0701000u, 0x80010a02u, 0xbf810000u}; + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + for (const bool ieee : {false, true}) { + Run(*device, Cases[index], Shaders[index], denorm16, ieee); + Check(Cases[index], denorm16, ieee); + } + } + } + std::puts("f16 denormal flush tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 7a4d392a4..5cf0e9a20 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -60,6 +60,10 @@ private: void write16Bits(const RdnaOperand& operand, IrU32 value); void writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); bool directedF16Rounding() const; + IrU32 flushF16Bits(IrU32 bits, std::uint32_t keepBit); + IrU32 flushF16Input(IrU32 bits); + IrU32 flushF16Result(IrU32 bits); + IrF32 flushF16InputValue(IrF32 value); IrU32 convertF32ToF16Bits(IrF32 value, std::initializer_list sources); void writeF16Conversion(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); IrU32 readU32(const RdnaOperand& operand); diff --git a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp index 880e7def8..6b76cb8f9 100644 --- a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp @@ -123,7 +123,7 @@ void TranslationContext::vCvtF16F32(const RdnaInstruction& inst) { } void TranslationContext::vCvtF32F16(const RdnaInstruction& inst) { - const IrF32 value = readF16AsF32(sourceAt(inst, 0u)); + const IrF32 value = flushF16InputValue(readF16AsF32(sourceAt(inst, 0u))); writeOperand(inst.destination, &value.Value()); } diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 2ca7925c1..318497431 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -88,12 +88,13 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc bool TranslationContext::float16Unary(const RdnaInstruction& inst, IrOpcode opcode) { const RdnaOperand& operand = sourceAt(inst, 0u); - const IrF32 argument = readF16AsF32(operand); + const IrF32 argument = flushF16InputValue(readF16AsF32(operand)); const auto [result, invalid] = unaryFloatSpecials(opcode, argument, IrF32(ir.Emit(opcode, IrType::F32, {&argument.Value()}))); IrU32 bits = packHalf2x16(applyF16ResultModifiers(inst.destination, clampF16Overflow(result, {&argument.Value()})), IrF32(ir.ConstantF32(0.0f))); if (opcode == IrOpcode::FPFract32) { bits = IrU32(ir.Select(ir.IEqual(bits.Value(), ir.Constant(0x3c00u)), ir.Constant(0x3bffu), bits.Value())); } + bits = flushF16Result(bits); const IrU32 half = readF16Bits(operand); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(half.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); const IrU32 special(ir.Select(nan.Value(), quietNan16(half).Value(), ir.Constant(0xfe00u))); @@ -327,11 +328,7 @@ IrU32 TranslationContext::normF32(IrU32 bits, bool signedValue) { bool TranslationContext::vLdexpF16(const RdnaInstruction& inst) { const IrU32 source = readF16Bits(sourceAt(inst, 0u)); - IrU32 bits = source; - if (floatMode.has_value() && (floatMode->floatMode & 0x40u) == 0u) { - const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); - bits = IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x8000u)), bits.Value())); - } + const IrU32 bits = flushF16Input(source); const IrF16 half(ir.Emit(IrOpcode::BitCastF16U16, IrType::F16, {&ir.Emit(IrOpcode::ConvertU16U32, IrType::U16, {&bits.Value()})})); const IrF32 value(ir.Emit(IrOpcode::ConvertF32F16, IrType::F32, {&half.Value()})); const RdnaOperand& exponentSource = sourceAt(inst, 1u); @@ -351,8 +348,8 @@ bool TranslationContext::vLdexpF16(const RdnaInstruction& inst) { } bool TranslationContext::vFrexpF16(const RdnaInstruction& inst, bool exponent) { - const IrU32 bits = readF16Bits(sourceAt(inst, 0u)); - const IrF32 value = readF16AsF32(sourceAt(inst, 0u)); + const IrU32 bits = flushF16Input(readF16Bits(sourceAt(inst, 0u))); + const IrF32 value = flushF16InputValue(readF16AsF32(sourceAt(inst, 0u))); const IrU32 word(ir.BitCastU32(value.Value())); const IrU1 special(ir.LogicalOr(ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0u)), ir.Emit(IrOpcode::UGreaterThanEqual32, IrType::U1, {&ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), &ir.Constant(0x7c00u)}))); @@ -371,7 +368,7 @@ bool TranslationContext::vFrexpF16(const RdnaInstruction& inst, bool exponent) { } bool TranslationContext::vCvtNormF16(const RdnaInstruction& inst, bool signedValue) { - write16Bits(inst.destination, normF16(readF16Bits(sourceAt(inst, 0u)), signedValue)); + write16Bits(inst.destination, normF16(flushF16Input(readF16Bits(sourceAt(inst, 0u))), signedValue)); return true; } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index e4e3de5c6..8347975fc 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -160,6 +160,27 @@ bool TranslationContext::directedF16Rounding() const { return floatMode.has_value() && ((floatMode->floatMode >> 2u) & 3u) != 0u && ((floatMode->floatMode >> 6u) & 3u) == 3u; } +IrU32 TranslationContext::flushF16Bits(IrU32 bits, std::uint32_t keepBit) { + if (!floatMode.has_value() || (floatMode->floatMode & keepBit) != 0u) return bits; + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); + return IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x8000u)), bits.Value())); +} + +IrU32 TranslationContext::flushF16Input(IrU32 bits) { + return flushF16Bits(bits, 0x40u); +} + +IrU32 TranslationContext::flushF16Result(IrU32 bits) { + return flushF16Bits(bits, 0x80u); +} + +IrF32 TranslationContext::flushF16InputValue(IrF32 value) { + if (!floatMode.has_value() || (floatMode->floatMode & 0x40u) != 0u) return value; + const IrU32 bits(ir.BitCastU32(value.Value())); + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x38800000u))); + return IrF32(ir.BitCastF32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value()))); +} + IrU32 TranslationContext::convertF32ToF16Bits(IrF32 value, std::initializer_list sources) { const IrF16 half(ir.Emit(IrOpcode::ConvertF16F32, IrType::F16, {&clampF16Overflow(value, sources).Value()})); const IrU32 native(ir.Emit(IrOpcode::ConvertU32U16, IrType::U32, {&ir.Emit(IrOpcode::BitCastU16F16, IrType::U16, {&half.Value()})})); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..1e386a73b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -471,6 +471,7 @@ Throughout the project, every function at every stage either **does exactly what - [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. Only f16 denormals kept with round to nearest even is translated; another f16 mode throws, as for packed f16. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. +- [f16 unary, frexp and conversion denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` are not measured and keep their previous lowering. Shaders without a known float mode keep the previous lowering. - [Packed f16 NaN results](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. - [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` is the exception: it preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. From 1462312ebf9dd620d38710827a12166acbae1f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 20:28:50 +0200 Subject: [PATCH 056/318] docs(savedata): record the inferred sceSaveDataConvert contract --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index aaffeaec6..37f478e76 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -126,6 +126,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures +- [sceSaveDataConvert](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataCancel](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached From 471e12e4843fe98406c3e65a4f41e06e011ded84 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:35:09 +0200 Subject: [PATCH 057/318] fix(libSceCesCs): drop the unused module target and move the TechnicalDebt line Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceCesCs-module/CMakeLists.txt | 3 --- docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 core/libs/prx/libSceCesCs-module/CMakeLists.txt diff --git a/core/libs/prx/libSceCesCs-module/CMakeLists.txt b/core/libs/prx/libSceCesCs-module/CMakeLists.txt deleted file mode 100644 index 4f216c2f6..000000000 --- a/core/libs/prx/libSceCesCs-module/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -add_library(libSceCesCs-module SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/../libSceCesCs/Export.cpp) -target_include_directories(libSceCesCs-module PRIVATE ${LIBS_INCLUDE_DIR}) -configure_windows_unwind(libSceCesCs-module) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1490f39cd..87112c296 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -39,12 +39,12 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and returns success for any `id`, like `sceHttp2SslEnableOption` (prosper returns the invalid-id error for an unknown one) - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. +- [sceCesMbcsUcsContextInit](../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns success without writing to the context, and returns the invalid-parameter error for a null context. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); the context layout and its size are unknown, so nothing is stored. PPSA15246 calls it and imports no other function that takes the context; no conversion through an initialized context exists. `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8` and `sceCesUtf8ToSbc` only, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), where byte 0x98 (unassigned) maps to U+0098 like the unassigned Cp1252 bytes; the console behavior for that byte is not verified - [sceRudpActivate](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified - [ulobjmgr](../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does - [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication - [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - [sceAudioInInit](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 without library-wide setup, as `sceAudioOutInit` does; each `sceAudioInOpen` opens its own SDL capture device -- [sceCesMbcsUcsContextInit](../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns success without writing to the context, and returns the invalid-parameter error for a null context. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); the context layout and its size are unknown, so nothing is stored. PPSA15246 calls it and imports no other function that takes the context; no conversion through an initialized context exists. `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8` and `sceCesUtf8ToSbc` only, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), where byte 0x98 (unassigned) maps to U+0098 like the unassigned Cp1252 bytes; the console behavior for that byte is not verified ### Unknown function info From b610642c613be93291abd4b61ec932d9fd3e07c6 Mon Sep 17 00:00:00 2001 From: Achraf Reyani Date: Mon, 12 Oct 2026 03:47:39 +0900 Subject: [PATCH 058/318] fix(libkernel): close an unopened descriptor on Windows without the invalid-parameter report close() of a descriptor that is not open calls _close, which raises the UCRT invalid-parameter handler before returning EBADF. In a relinked title that handler is libc's crash reporter, so an ordinary close of a closed descriptor printed a FATAL report and every thread's stack. The call now runs with the thread-local no-op handler, as dup and dup2 already do. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- core/libs/tests/GuestDup.cpp | 23 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 255a3d9fe..1f6ec80e0 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -447,7 +447,7 @@ int APS5_VABI close_nid_postfix(int d) { #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); - return _close(d); + return WithoutParameterHandler([d] { return ::_close(d); }); #else return ::close(d); #endif diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 1e3683f92..61d280395 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -165,7 +165,30 @@ static void RandomDeviceDuplicates() { for (const int descriptor : {random, dupRandom, pipe[1], secondRandom}) Require(close_nid_postfix(descriptor) == 0); } +#ifdef _WIN32 +static bool g_invalidParameter = false; +static void RecordInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) { g_invalidParameter = true; } +#endif +static void CloseRejectsClosedDescriptors() { + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(close_nid_postfix(pipe[1]) == 0); +#ifdef _WIN32 + const auto previous = _set_invalid_parameter_handler(RecordInvalidParameter); +#endif + *__error_nid_postfix() = 0; + RequireFailure(close_nid_postfix(pipe[1]), GuestEbadf); + *__error_nid_postfix() = 0; + RequireFailure(close_nid_postfix(-1), GuestEbadf); +#ifdef _WIN32 + _set_invalid_parameter_handler(previous); + Require(!g_invalidParameter); +#endif + Require(close_nid_postfix(pipe[0]) == 0); +} + int main() { + CloseRejectsClosedDescriptors(); DupOutlivesOriginal(); Dup2ReplacesTarget(); BadDescriptors(); From 471d9eced22fccbcc8908654abf55f8f15335f22 Mon Sep 17 00:00:00 2001 From: Avijit Singh Date: Mon, 12 Oct 2026 00:19:21 +0530 Subject: [PATCH 059/318] fix(agc): keep write tracker alive through driver teardown Initialize the guest write tracker during Driver construction so its function-local static is destroyed after Driver on process exit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 1dbe1cfd1..7201c4124 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -55,6 +55,8 @@ void Driver::stop() { } Driver::Driver() { + // Driver teardown uses the write tracker; initialize it before this function-local static completes. + static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { LibcRegisterShutdown_nid_postfix([] { Driver::Get().Shutdown(); }); From 2e0dcd7f57ccc9aab0fec0b017f693ce6e64ae40 Mon Sep 17 00:00:00 2001 From: Avijit Singh Date: Mon, 12 Oct 2026 00:19:27 +0530 Subject: [PATCH 060/318] docs(agc): record write tracker shutdown ordering Document why the Driver singleton must initialize the guest write tracker before its static construction completes on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/dev/TechnicalDebt.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..9863562e2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -9,6 +9,10 @@ - libwinpthread-1.dll - winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) are locked once when created. The host `std::shared_mutex` objects in [Recompiler.cpp](../../core/shader/recompiler/Recompiler.cpp) and [GuestWriteWatch.cpp](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) are not, and can still race on their first contended use. +### Shutdown + +- On Windows, console-control termination can run function-local static destructors through `ExitProcess`. `Driver::~Driver()` stops GPU state, which can access the guest write tracker. `Driver::Driver()` therefore initializes the tracker before the `Driver` singleton finishes construction, so reverse static-destruction order keeps the tracker alive through driver teardown. + ### Silent stubs Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: From bc5bfd0ed631e4bd22b1b2eb0ec94c53f22ff6fc Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 18:01:22 +0000 Subject: [PATCH 061/318] feat(libkernel): export setuid, setreuid, setgroups and getgroups `setuid`, `setreuid` and `setgroups` are absent imports of XPSemu (PPSA97358) and PS5MC (PPSA99420) on `main`. The guest process is modelled as root with ids 0, as `seteuid` and `setegid` already assume: `setuid` accepts 0 and `setreuid` 0 or -1 per id and change nothing, any other id throws. `setgroups` keeps the supplementary group list as FreeBSD's `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 throws) and the new `getgroups` reports it. `guest_credentials` covers the accepted ids, the default list, a set list, an empty list and the EINVAL/EFAULT cases. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit d12b062074dcf64bff694800d77f7261adf6f59e) --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ .../libs/prx/libkernel/System/src/Process.cpp | 56 +++++++++++++++ core/libs/tests/GuestCredentials.cpp | 68 +++++++++++++------ docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 114 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..7e1d1d809 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -206,6 +206,16 @@ if(BUILD_TESTING) set_tests_properties(guest_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_credentials_tests ${LIBS_INCLUDE_DIR}/tests/GuestCredentials.cpp) + target_include_directories(guest_credentials_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_credentials_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_credentials_tests) + add_test(NAME guest_credentials COMMAND guest_credentials_tests) + set_tests_properties(guest_credentials PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_credentials PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_truncate_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 45528cd7a..0b3fb9674 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -18,6 +18,7 @@ #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include #include #include #include @@ -44,7 +45,11 @@ namespace { constexpr int sceInvalidArgument = static_cast(0x80020016u); constexpr int errnoNoChild = 10; +constexpr int errnoBadAddress = 14; constexpr int errnoInvalidArgument = 22; +constexpr int freebsdGroupsMax = 1023; +std::mutex supplementaryGroupsMutex; +std::vector supplementaryGroups{0}; constexpr unsigned freebsdWaitOptions = 0x8000003Fu; std::atomic gpoBits{0}; @@ -473,4 +478,55 @@ int APS5_VABI setegid_nid_postfix(std::uint32_t egid) { return 0; } +int APS5_VABI setuid_nid_postfix(std::uint32_t uid) { + if (uid != 0) + throw std::runtime_error("setuid: unsupported user id " + std::to_string(uid)); + return 0; +} + +int APS5_VABI setreuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (ruid != 0 && ruid != unchanged) + throw std::runtime_error("setreuid: unsupported real user id " + std::to_string(ruid)); + if (euid != 0 && euid != unchanged) + throw std::runtime_error("setreuid: unsupported effective user id " + std::to_string(euid)); + return 0; +} + +int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { + if (count < 0 || count > freebsdGroupsMax) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } + if (count > 0 && groups == nullptr) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + if (count > 0 && groups[0] != 0) + throw std::runtime_error("setgroups: unsupported effective group id " + std::to_string(groups[0])); + std::lock_guard lock(supplementaryGroupsMutex); + if (count == 0) { + supplementaryGroups.resize(1); + return 0; + } + supplementaryGroups.assign(groups, groups + count); + return 0; +} + +int APS5_VABI getgroups_nid_postfix(int capacity, std::uint32_t* groups) { + std::lock_guard lock(supplementaryGroupsMutex); + const int count = static_cast(supplementaryGroups.size()); + if (capacity == 0) return count; + if (capacity < count) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } + if (groups == nullptr) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + std::copy(supplementaryGroups.begin(), supplementaryGroups.end(), groups); + return count; +} + } diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index aa2c8544f..b401588d6 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -1,31 +1,57 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include -#include + extern "C" { -int APS5_VABI seteuid_nid_postfix(std::uint32_t); -int APS5_VABI setegid_nid_postfix(std::uint32_t); +int APS5_VABI setuid_nid_postfix(std::uint32_t); +int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); +int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); +int APS5_VABI getgroups_nid_postfix(int, std::uint32_t*); +int APS5_VABI getuid_nid_postfix(void); +int APS5_VABI geteuid_nid_postfix(void); +int APS5_VABI getegid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); } -using SetId = int (APS5_VABI *)(std::uint32_t); + static void Require(bool value) { if (!value) std::abort(); } -static bool Throws(SetId function, std::uint32_t id) { - try { - function(id); - } catch (const std::runtime_error&) { - return true; - } - return false; + +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr std::uint32_t Unchanged = 0xffffffffu; + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); } + int main() { - *__error_nid_postfix() = 13; - Require(seteuid_nid_postfix(0) == 0); - Require(setegid_nid_postfix(0) == 0); - Require(*__error_nid_postfix() == 13); - Require(Throws(seteuid_nid_postfix, 1000)); - Require(Throws(setegid_nid_postfix, 1000)); - Require(Throws(seteuid_nid_postfix, 0xFFFFFFFFu)); - Require(Throws(setegid_nid_postfix, 0xFFFFFFFFu)); - Require(seteuid_nid_postfix(0) == 0); - Require(setegid_nid_postfix(0) == 0); + Require(setuid_nid_postfix(0) == 0); + Require(getuid_nid_postfix() == 0 && geteuid_nid_postfix() == 0); + Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); + Require(setreuid_nid_postfix(0, Unchanged) == 0); + Require(setreuid_nid_postfix(Unchanged, 0) == 0); + Require(setreuid_nid_postfix(0, 0) == 0); + + std::array groups{}; + Require(getgroups_nid_postfix(0, nullptr) == 1); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); + Require(groups[0] == static_cast(getegid_nid_postfix())); + + const std::array wanted{0, 5, 7}; + Require(setgroups_nid_postfix(static_cast(wanted.size()), wanted.data()) == 0); + Require(getgroups_nid_postfix(0, nullptr) == 3); + RequireFailure(getgroups_nid_postfix(2, groups.data()), GuestEinval); + RequireFailure(getgroups_nid_postfix(3, nullptr), GuestEfault); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 3); + Require(groups[0] == 0 && groups[1] == 5 && groups[2] == 7); + + Require(setgroups_nid_postfix(0, nullptr) == 0); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); + + RequireFailure(setgroups_nid_postfix(-1, wanted.data()), GuestEinval); + RequireFailure(setgroups_nid_postfix(1024, wanted.data()), GuestEinval); + RequireFailure(setgroups_nid_postfix(1, nullptr), GuestEfault); + Require(getgroups_nid_postfix(0, nullptr) == 1); + return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..c6e0af03d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -197,7 +197,7 @@ Throughout the project, every function at every stage either **does exactly what - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` are always readable and writable, as FreeBSD reports a seeded random device. Other descriptors that are not libSceNet sockets throw. Dungeons (PPSA16064) selects on descriptor 0 - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it -- [seteuid, setegid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid` and `sys_setegid`, id 0 is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change the effective id, which is not modelled, and throws. The ids and privileges a title has on a console are unverified +- [seteuid, setegid, setuid, setreuid, setgroups, getgroups](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid` and `sys_setreuid`, id 0 (or -1 for an unchanged `setreuid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified - [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED is accepted on the assumption that the console follows FreeBSD 11.0 libthr, which accepts it (9.0 rejects it with EINVAL); unverified on hardware. Guest code runs in one process, so a shared barrier behaves as a private one - [pthread_getname_np](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) on a PS5 (firmware 13.60), but its object is freed after the join here, so the pointer cannot be told apart from a live thread and its name is read as for a live one - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature From b6f96f8e90893c11b1cc68bb33688fa334ca802a Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 18:55:56 +0000 Subject: [PATCH 062/318] test(libkernel): extend existing credential regression coverage --- core/libs/prx/libkernel/CMakeLists.txt | 10 ---------- core/libs/tests/GuestCredentials.cpp | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 7e1d1d809..41cb24584 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -206,16 +206,6 @@ if(BUILD_TESTING) set_tests_properties(guest_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_test_executable(guest_credentials_tests ${LIBS_INCLUDE_DIR}/tests/GuestCredentials.cpp) - target_include_directories(guest_credentials_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_credentials_tests PRIVATE libkernel libc) - configure_windows_unwind(guest_credentials_tests) - add_test(NAME guest_credentials COMMAND guest_credentials_tests) - set_tests_properties(guest_credentials PROPERTIES TIMEOUT 20) - if(WIN32) - set_tests_properties(guest_credentials PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_truncate_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index b401588d6..99e003528 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -2,8 +2,11 @@ #include #include #include +#include extern "C" { +int APS5_VABI seteuid_nid_postfix(std::uint32_t); +int APS5_VABI setegid_nid_postfix(std::uint32_t); int APS5_VABI setuid_nid_postfix(std::uint32_t); int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); @@ -15,6 +18,16 @@ int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } +using SetId = int (APS5_VABI *)(std::uint32_t); + +static bool Throws(SetId function, std::uint32_t id) { + try { + function(id); + } catch (const std::runtime_error&) { + return true; + } + return false; +} constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; @@ -26,6 +39,15 @@ static void RequireFailure(int result, int error) { } int main() { + *__error_nid_postfix() = 13; + Require(seteuid_nid_postfix(0) == 0); + Require(setegid_nid_postfix(0) == 0); + Require(*__error_nid_postfix() == 13); + Require(Throws(seteuid_nid_postfix, 1000)); + Require(Throws(setegid_nid_postfix, 1000)); + Require(Throws(seteuid_nid_postfix, Unchanged)); + Require(Throws(setegid_nid_postfix, Unchanged)); + Require(setuid_nid_postfix(0) == 0); Require(getuid_nid_postfix() == 0 && geteuid_nid_postfix() == 0); Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); From a89aa5c0d0c7e8442b20ddebccccdcb2ac27ac48 Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:59:41 -0700 Subject: [PATCH 063/318] fix(progress): skip cells for rectangles without area When squarify gave a group a sliver narrower than 2px, treemap passed cells a zero or negative width, so the row count became complex or divided by zero and render crashed. cells now returns no cells for rectangles without area. A regression test renders a 300 and 1 item split. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01V9UU71SP1UKGR51Sowbgmq --- CMakeLists.txt | 2 ++ tools/progress.py | 2 +- tools/tests/test_progress_render_sliver.py | 24 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tools/tests/test_progress_render_sliver.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb4fb85c..8935c172d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress.py) add_test(NAME progress_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_encoding.py) + add_test(NAME progress_render_sliver COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_render_sliver.py) add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE} diff --git a/tools/progress.py b/tools/progress.py index 6f37f6cce..57fe7e175 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -256,7 +256,7 @@ def squarify(values, x, y, w, h): def cells(count, x, y, w, h): - if not count: + if not count or w <= 0 or h <= 0: return [] rows = max(1, min(count, round((count * h / w) ** 0.5))) result = [] diff --git a/tools/tests/test_progress_render_sliver.py b/tools/tests/test_progress_render_sliver.py new file mode 100644 index 000000000..4fa8b2194 --- /dev/null +++ b/tools/tests/test_progress_render_sliver.py @@ -0,0 +1,24 @@ +import importlib.util +import unittest +from pathlib import Path + +SPEC = importlib.util.spec_from_file_location("progress", Path(__file__).resolve().parents[1] / "progress.py") +progress = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(progress) + + +class ProgressRenderSliverTests(unittest.TestCase): + def test_render_with_sliver_group(self): + libraries = {"done": 301, "total": 301, "percent": 100, "groups": [ + {"name": "libA", "label": "libA", "done": 300, "todo": 0}, + {"name": "libB", "label": "libB", "done": 1, "todo": 0}]} + shaders = {"done": 0, "total": 0, "percent": 0, "groups": []} + self.assertIn(" Date: Sun, 11 Oct 2026 12:03:50 -0700 Subject: [PATCH 064/318] fix(tools): ignore wrapped declarations in pr_overlap exports exports() only checked whether the matching line ended with a semicolon, so a prototype wrapped over several lines was counted as an implemented export and two PRs adding the same forward declaration were flagged as overlapping. It now joins the following added lines up to the first semicolon or brace and treats the match as a declaration when the semicolon comes first. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01V9UU71SP1UKGR51Sowbgmq --- CMakeLists.txt | 2 ++ tools/pr_overlap.py | 13 +++++++-- .../tests/test_pr_overlap_wrapped_exports.py | 28 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tools/tests/test_pr_overlap_wrapped_exports.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb4fb85c..53577c535 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py) add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py) + add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) endif() diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index e84ee058d..82aa9b527 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -70,9 +70,18 @@ def commit(tree): def exports(diff): found = set() - for line in diff.splitlines(): + lines = diff.splitlines() + for i, line in enumerate(lines): m = EXPORT.match(line) - if m and not m.group(1).endswith("_nid_no_patch") and not line.rstrip().endswith(";"): + if not m or m.group(1).endswith("_nid_no_patch"): + continue + text = line[1:] + for nxt in lines[i + 1:]: + if ";" in text or "{" in text or not nxt.startswith("+") or nxt.startswith("+++"): + break + text += nxt[1:] + semi, brace = text.find(";"), text.find("{") + if semi < 0 or 0 <= brace < semi: found.add(m.group(1)) return found diff --git a/tools/tests/test_pr_overlap_wrapped_exports.py b/tools/tests/test_pr_overlap_wrapped_exports.py new file mode 100644 index 000000000..96dbd2232 --- /dev/null +++ b/tools/tests/test_pr_overlap_wrapped_exports.py @@ -0,0 +1,28 @@ +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + +DECL = "+int APS5_VABI sceFooBar(int a,\n+ int b);\n" + + +class WrappedExportTests(unittest.TestCase): + def test_wrapped_declaration_is_not_export(self): + self.assertEqual(pr_overlap.exports(DECL), set()) + + def test_wrapped_definition_is_export(self): + diff = "+int APS5_VABI sceFooBar(int a,\n+ int b) {\n+ return a + b;\n+}\n" + self.assertEqual(pr_overlap.exports(diff), {"sceFooBar"}) + + def test_declaration_then_definition_returns_only_definition(self): + diff = DECL + "+int APS5_VABI sceBaz(int a) { return a; }\n" + self.assertEqual(pr_overlap.exports(diff), {"sceBaz"}) + + def test_declaration_at_end_of_diff(self): + self.assertEqual(pr_overlap.exports("+int APS5_VABI sceFooBar(int a, int b);"), set()) + + +if __name__ == "__main__": + unittest.main() From 6eacac2e96fb8ccfb37659d12878be14e2d1b0f5 Mon Sep 17 00:00:00 2001 From: Achraf Reyani Date: Mon, 12 Oct 2026 04:08:23 +0900 Subject: [PATCH 065/318] test(libkernel): check close of an unopened descriptor among the other bad-descriptor cases Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestDup.cpp | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 61d280395..1388a600d 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -81,6 +81,10 @@ static void Dup2ReplacesTarget() { for (const int descriptor : {first[0], first[1], second[0], second[1]}) Require(close_nid_postfix(descriptor) == 0); } +#ifdef _WIN32 +static bool g_invalidParameter = false; +static void RecordInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) { g_invalidParameter = true; } +#endif static void BadDescriptors() { int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); @@ -94,6 +98,15 @@ static void BadDescriptors() { RequireFailure(dup2_nid_postfix(-1, pipe[0]), GuestEbadf); RequireFailure(dup_nid_postfix(FirstSocket + 0x0ffffff0), GuestEbadf); RequireFailure(dup2_nid_postfix(FirstSocket + 0x0ffffff0, FirstSocket + 0x0ffffff1), GuestEbadf); +#ifdef _WIN32 + const auto previous = _set_invalid_parameter_handler(RecordInvalidParameter); +#endif + RequireFailure(close_nid_postfix(closed), GuestEbadf); + RequireFailure(close_nid_postfix(-1), GuestEbadf); +#ifdef _WIN32 + _set_invalid_parameter_handler(previous); + Require(!g_invalidParameter); +#endif Require(close_nid_postfix(pipe[0]) == 0); } @@ -165,30 +178,7 @@ static void RandomDeviceDuplicates() { for (const int descriptor : {random, dupRandom, pipe[1], secondRandom}) Require(close_nid_postfix(descriptor) == 0); } -#ifdef _WIN32 -static bool g_invalidParameter = false; -static void RecordInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) { g_invalidParameter = true; } -#endif -static void CloseRejectsClosedDescriptors() { - int pipe[2]; - Require(pipe_nid_postfix(pipe) == 0); - Require(close_nid_postfix(pipe[1]) == 0); -#ifdef _WIN32 - const auto previous = _set_invalid_parameter_handler(RecordInvalidParameter); -#endif - *__error_nid_postfix() = 0; - RequireFailure(close_nid_postfix(pipe[1]), GuestEbadf); - *__error_nid_postfix() = 0; - RequireFailure(close_nid_postfix(-1), GuestEbadf); -#ifdef _WIN32 - _set_invalid_parameter_handler(previous); - Require(!g_invalidParameter); -#endif - Require(close_nid_postfix(pipe[0]) == 0); -} - int main() { - CloseRejectsClosedDescriptors(); DupOutlivesOriginal(); Dup2ReplacesTarget(); BadDescriptors(); From 99edf9435c6ae8535ed8059f9c2ce798a7bd590f Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:25:58 +0200 Subject: [PATCH 066/318] fix(recompiler): read P10 of a flat input as the provoking vertex A FLAT_SHADE input without pass-through that a pixel shader reads through v_interp_mov_f32 P10 became an explicit per-vertex input and read vertex 1 minus vertex 0. With the last provoking vertex the pipeline then threw ("last provoking vertex with explicit per-vertex flat interpolation is unsupported"), which stopped all three ps5-opengl titles in the catalog. ps5-opengl reads every flat input through P10 when GL's last-vertex convention is in effect, and its driver passes the GL 4.6 CTS on a PS5 (#1413). Such a read now returns the provoking vertex's value, as P0 does, and only a P20 read keeps a flat input per vertex. The lane was not measured on the console itself, so FragmentParameter records it and a pipeline using the first provoking vertex throws. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 8 +++++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 8 ++++++++ .../Optimization/src/ShaderInfoCollector.cpp | 2 +- core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 2 ++ .../src/SpirvMemory/SpirvInputOutput.cpp | 18 ++++++++++++++++-- docs/dev/TechnicalDebt.md | 2 +- 7 files changed, 36 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d47dcb3da..920c2124a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -94,7 +94,13 @@ void ValidateDepthBounds(const Context& context, const State& state) { void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders) { Require(state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT || state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, "invalid provoking vertex mode"); - if (state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) return; + if (state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) { + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; + Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flatP10; }), "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); + } + return; + } Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); Require(!state.rectList && state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines"); Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..03a2c1da0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3334,6 +3334,14 @@ void ProvokingVertexTests() { Require(flatPixel.fragmentParameters.size() == 1 && flatPixel.fragmentParameters[0].flat && !flatPixel.fragmentParameters[0].perVertex, "flat v_interp_mov P0 has explicit per-vertex metadata"); const std::array flatShaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatPixel, 0}}}; ValidateProvokingVertex(supported, last, flatShaders); + const std::array flatP10Color{0xc8120000u, 0xc8160100u, 0xc81a0200u, 0xc81e0300u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto flatP10Pixel = recompilePixel({0x400u}, flatP10Color); + const auto p10Inputs = locatedInputs(flatP10Pixel.spirv.Words()); + Require(p10Inputs.size() == 1 && p10Inputs[0].location == 0 && p10Inputs[0].flat && !p10Inputs[0].perVertex, "flat v_interp_mov P10 did not compile to a flat input"); + Require(flatP10Pixel.fragmentParameters.size() == 1 && flatP10Pixel.fragmentParameters[0].flatP10 && !flatP10Pixel.fragmentParameters[0].perVertex && !flatPixel.fragmentParameters[0].flatP10, "flat v_interp_mov P10 was not recorded apart from P0"); + const std::array flatP10Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP10Pixel, 0}}}; + ValidateProvokingVertex(supported, last, flatP10Shaders); + expectFailure([&] { ValidateProvokingVertex(supported, first, flatP10Shaders); }, "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); ShaderRecompiler::RecompileResult fragment{}; const std::array shaders{{{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 00bde0c18..5f286eb08 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -257,7 +257,7 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const auto mode = inst->Argument(2)->Resolve()->ImmediateU32(); read[input] = true; - perVertex[input] = perVertex[input] || mode < 2u || !IsPixelParameterFlat(*pixel, input); + perVertex[input] = perVertex[input] || mode == 1u || !IsPixelParameterFlat(*pixel, input); } else if (inst->Opcode() == IrOpcode::GetInterpolationParameterF16) { const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const bool high = inst->Argument(3)->Resolve()->ImmediateU32() != 0u; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index deca904d9..fcd5af807 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -387,6 +387,7 @@ struct FragmentParameter { bool flat; bool perVertex; bool custom = false; + bool flatP10 = false; }; struct BarycentricEmulation { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index bd05440bc..bbca7b425 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -322,6 +322,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { out.Value(parameter.flat); out.Value(parameter.perVertex); out.Value(parameter.custom); + out.Value(parameter.flatP10); }); writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); @@ -364,6 +365,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { in.Value(parameter.flat); in.Value(parameter.perVertex); in.Value(parameter.custom); + in.Value(parameter.flatP10); }); reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 2c3eb07f5..a7a465d86 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -48,11 +48,25 @@ namespace ShaderRecompiler if (inputInfo.pixel == nullptr) FailEmit("pixel input info is missing"); const auto& pixel = *inputInfo.pixel; std::vector result; + std::array readsP10{}; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::GetInterpolationParameter || instruction->Argument(2)->Resolve()->ImmediateU32() != 0u) continue; + const auto attribute = instruction->Argument(0)->Resolve()->ImmediateU32(); + if (attribute < readsP10.size()) readsP10[attribute] = true; + } + } for (const auto& input : program.Info().inputs) { if (input.kind != StageInputKind::Parameter) continue; const auto location = PixelInputLocation(pixel, input.location); - if (std::any_of(result.begin(), result.end(), [&](const auto& parameter) { return parameter.location == location; })) continue; - result.push_back({location, location, PixelInputIsFlat(pixel, input.location), input.perVertex, PixelInputIsCustom(pixel, input.location)}); + const bool flat = PixelInputIsFlat(pixel, input.location); + const bool flatP10 = flat && !input.perVertex && input.location < readsP10.size() && readsP10[input.location]; + const auto existing = std::find_if(result.begin(), result.end(), [&](const auto& parameter) { return parameter.location == location; }); + if (existing != result.end()) { + existing->flatP10 = existing->flatP10 || flatP10; + continue; + } + result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10}); } return result; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..cf63c2212 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -477,7 +477,7 @@ Throughout the project, every function at every stage either **does exactly what - [Overlay output bus](../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_log_f32 and v_log_f16](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Dual-source blending](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. From fdf288f3fc511e45319b8ecde855bad62819e7b5 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 8 Oct 2026 16:00:16 +0000 Subject: [PATCH 067/318] fix(libc): return fclose failures to callers (cherry picked from commit 610d6cdda098e9adfcd3814b52f8eba01caaa4d1) --- core/libs/prx/libc/include/FileStream.hpp | 5 +++-- core/libs/prx/libc/src/FileAndHeap.cpp | 3 +-- core/libs/tests/GuestFiopen.cpp | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 7a1b467be..0332062df 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -122,11 +122,12 @@ public: SyncStatus(); } - void Close() { + int Close() { GetHandle(); - if (std::fclose(std::exchange(_handle, nullptr)) != 0) throw std::runtime_error("FileStream: close failed"); + const int result = std::fclose(std::exchange(_handle, nullptr)); _guest.flags = 0; _guest.descriptor = -1; + return result; } }; static_assert(std::is_standard_layout_v); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 20527e876..834692101 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -146,8 +146,7 @@ int APS5_VABI fopen_s_nid_postfix(FileStream** result, const char* filename, con int APS5_VABI fclose_nid_postfix(FileStream* stream) { GetNativeStream(stream); std::unique_ptr owner(stream->IsDynamic() ? stream : nullptr); - stream->Close(); - return 0; + return stream->Close(); } int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int origin); diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 6c2636ae3..99dcbb5aa 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -131,6 +131,14 @@ int main() { Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'k'); fclose_nid_postfix(stream); +#ifndef _WIN32 + auto* full = std::fopen("/dev/full", "wb"); + Require(full != nullptr); + auto* fullStream = new FileStream(full, true); + Require(std::fwrite("buffered", 1, 8, full) == 8); + Require(fclose_nid_postfix(fullStream) == EOF); +#endif + std::filesystem::remove_all(dir); return 0; } From cc7343daf561542ce3be1dcaecffce72ced5e5e5 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 7 Oct 2026 23:12:33 +0000 Subject: [PATCH 068/318] fix(libc): clear fopen_s output on invalid arguments (cherry picked from commit 387279ee941e5a203325852b24cdb87e2f835e43) --- core/libs/prx/libc/src/FileAndHeap.cpp | 4 +++- core/libs/tests/GuestFiopen.cpp | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 20527e876..4b23cd2f0 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -138,7 +138,9 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) int APS5_VABI fopen_s_nid_postfix(FileStream** result, const char* filename, const char* mode) { constexpr int GuestEinval = 22; constexpr int GuestEnoent = 2; - if (!result || !filename || !mode) return GuestEinval; + if (!result) return GuestEinval; + *result = nullptr; + if (!filename || !mode) return GuestEinval; *result = fopen_nid_postfix(filename, mode); return *result ? 0 : GuestEnoent; } diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 6c2636ae3..86b28cb98 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -10,6 +10,7 @@ extern "C" { FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int); FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fopen_s_nid_postfix(FileStream**, const char*, const char*); int APS5_VABI fclose_nid_postfix(FileStream*); std::int64_t APS5_VABI ftello_nid_postfix(FileStream*); } @@ -24,6 +25,8 @@ static void Check(bool value, int line) { constexpr int In = 0x01, Out = 0x02, Ate = 0x04, App = 0x08, Trunc = 0x10, Nocreate = 0x20, Noreplace = 0x40, Binary = 0x80; +constexpr int GuestEinval = 22; +constexpr int GuestEnoent = 2; static FileStream* Open(const char* name, int mode) { return _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(name, mode, 0x1b6); @@ -66,6 +69,21 @@ int main() { Write(Open(file, Out), "abc"); Require(Contents(file) == "abc"); + FileStream* checkedStream = &_Stdout_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, nullptr, "rb") == GuestEinval); + Require(checkedStream == nullptr); + checkedStream = &_Stderr_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, file, nullptr) == GuestEinval); + Require(checkedStream == nullptr); + Require(fopen_s_nid_postfix(nullptr, file, "rb") == GuestEinval); + checkedStream = &_Stdout_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, file, "rb") == 0); + Require(checkedStream != nullptr); + Require(fclose_nid_postfix(checkedStream) == 0); + checkedStream = &_Stdout_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, missing, "rb") == GuestEnoent); + Require(checkedStream == nullptr); + auto* stream = Open(file, In); Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'a'); fclose_nid_postfix(stream); From ede6fad8b2a0c0a2823e1b694854e3a85278e402 Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:20:36 -0700 Subject: [PATCH 069/318] fix(conventions): treat wrapped declarations as declarations Check.duplicates reported a duplicate export for a hit in another library unless that single grep line ended with a semicolon, so a prototype wrapped over two lines was reported as a duplicate definition. The hit file is now read at the head commit and the existing function_body decides whether the hit is a definition. The git grep pattern also used the GNU escapes \b and \s, which the BSD regex library on macOS does not support, so the check never matched there. The pattern now uses POSIX classes instead. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01V9UU71SP1UKGR51Sowbgmq --- CMakeLists.txt | 2 + tools/check_conventions.py | 10 ++-- ..._check_conventions_wrapped_declarations.py | 57 +++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 tools/tests/test_check_conventions_wrapped_declarations.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb4fb85c..eaad726a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py) + add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 8254975e8..bf4512f36 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -116,14 +116,14 @@ class Check: def duplicates(self, exports): if not exports: return - pattern = r"\bAPS5_VABI\s+(" + "|".join(exports) + r")\s*\(" + pattern = r"(^|[^[:alnum:]_])APS5_VABI[[:space:]]+(" + "|".join(exports) + r")[[:space:]]*\(" result = subprocess.run(["git", "grep", "-nE", pattern, self.head, "--", "core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") for line in result.stdout.splitlines(): - _, path, _, text = line.split(":", 3) + _, path, number, text = line.split(":", 3) name = EXPORT.search(text).group(1) - own, number = exports[name] - if library(path) != library(own) and not text.rstrip().endswith(";"): - self.report("duplicate-export", own, number, f"{name} is also in {library(path)}") + own, own_number = exports[name] + if library(path) != library(own) and function_body(self.show(path).splitlines(), int(number) - 1) is not None: + self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def build(self): for path, lines in self.files(lambda p: p.name == "CMakeLists.txt" or p.suffix == ".cmake"): diff --git a/tools/tests/test_check_conventions_wrapped_declarations.py b/tools/tests/test_check_conventions_wrapped_declarations.py new file mode 100644 index 000000000..57df9623e --- /dev/null +++ b/tools/tests/test_check_conventions_wrapped_declarations.py @@ -0,0 +1,57 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +CHECKER = Path(__file__).resolve().parents[1] / "check_conventions.py" +DEFINITION = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" + + +class WrappedDeclarationTests(unittest.TestCase): + def check(self, first_file, first_text): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + git("init", "-q") + first = root / "core/libs/prx/libSceA" / first_file + first.parent.mkdir(parents=True) + first.write_text(first_text, encoding="utf-8") + other = root / "core/libs/prx/libSceA/Other.cpp" + other.write_text("int APS5_VABI sceOther() { return 1; }\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add library a") + second = root / "core/libs/prx/libSceB/Export.cpp" + second.parent.mkdir(parents=True) + second.write_text(DEFINITION, encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add library b") + env = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + env.update(GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="safe.directory", GIT_CONFIG_VALUE_0=str(root)) + return subprocess.run( + [sys.executable, "-I", str(CHECKER), "--base", "HEAD~1", "--head", "HEAD"], + cwd=root, capture_output=True, text=True, env=env, timeout=20, + ) + + def test_wrapped_declaration_is_not_a_duplicate(self): + result = self.check("Export.h", "int APS5_VABI sceFooBar(int a,\n int b);\n") + self.assertNotIn("duplicate-export", result.stdout, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_wrapped_definition_is_a_duplicate(self): + result = self.check("Export.cpp", "int APS5_VABI sceFooBar(int a,\n int b) {\n return 0;\n}\n") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[duplicate-export]", result.stdout) + + +if __name__ == "__main__": + unittest.main() From 2a58fb7997e6ad72d9e6f21f85c970e569a77b94 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sun, 11 Oct 2026 16:41:55 -0300 Subject: [PATCH 070/318] fix(shader): preserve distinct definitions while folding phi inputs Fold identical resolved values or equal immediate constants, retaining distinct branch-local SSA definitions. AI-assisted: yes (Codex). --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 +++ .../prx/libSceAgcDriver/tests/ConstantPhi.cpp | 91 +++++++++++++++++++ .../Optimization/src/ConstantFolder.cpp | 2 +- 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..4de0fef0b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1567,3 +1567,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_constant_phi_tests EXCLUDE_FROM_ALL tests/ConstantPhi.cpp) +target_link_libraries(agc_constant_phi_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_constant_phi_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_constant_phi_tests) +if(BUILD_TESTING) + set_target_properties(agc_constant_phi_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_constant_phi COMMAND agc_constant_phi_tests) + if(WIN32) + set_tests_properties(agc_constant_phi PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp new file mode 100644 index 000000000..1a811755b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp @@ -0,0 +1,91 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckDiamond(unsigned mode) { + IrProgram program; + for (std::uint32_t id = 0; id < 4u; ++id) { + auto& block = program.CreateBlock(); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = id; + info.terminator.kind = TerminatorKind::Return; + program.Metadata().blockInfo.push_back(info); + } + auto& entry = *program.BlockOrder()[0]; + auto& left = *program.BlockOrder()[1]; + auto& right = *program.BlockOrder()[2]; + auto& merge = *program.BlockOrder()[3]; + program.SetEntryBlock(entry); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + auto& entryInfo = program.Metadata().blockInfo[0]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.trueBlock = 1u; + entryInfo.terminator.falseBlock = 2u; + for (std::uint32_t id = 1; id < 3u; ++id) { + program.Metadata().blockInfo[id].terminator.kind = TerminatorKind::Branch; + program.Metadata().blockInfo[id].terminator.trueBlock = 3u; + } + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& limit = builder.Constant(16u); + auto& condition = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &limit}); + entryInfo.condition = &condition; + auto& one = builder.Constant(1u); + auto& two = builder.Constant(2u); + IrValue* first = &lane; + IrValue* second = &lane; + if (mode == 0u || mode == 1u) { + builder.SetInsertionPoint(left); + first = mode == 0u ? &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &one, &two}) : &builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + builder.SetInsertionPoint(right); + second = mode == 0u ? &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &one, &two}) : &builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + } else if (mode == 3u || mode == 4u) { + first = &one; + second = &program.CreateValue(IrOpcode::Void, IrType::U32); + second->SetImmediateU32(mode == 3u ? 1u : 2u); + } + builder.SetInsertionPoint(merge); + auto& phi = builder.Emit(IrOpcode::Phi, IrType::U32, {}); + phi.AddPhiOperand(&left, first); + phi.AddPhiOperand(&right, second); + auto& reference = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&phi}); + ValidateProgram(program, true); + ConstantFolder{}.Fold(program); + DeadCodeEliminator{}.RemoveIdentities(program); + DeadCodeEliminator{}.Eliminate(program); + ValidateProgram(program, true); + const auto* result = reference.Argument(0)->Resolve(); + if (mode == 0u || mode == 1u || mode == 4u) { + Require(result == &phi && phi.Argument(0) == first && phi.Argument(1) == second, "phi lost distinct branch definitions or constants"); + } else { + Require(result == first, "phi did not fold identical definitions or equal constants"); + } +} + +} + +int main() { + try { + for (unsigned mode = 0; mode < 5u; ++mode) CheckDiamond(mode); + std::puts("constant phi dominance tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 0020b87cb..bce47c73f 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -144,7 +144,7 @@ bool foldPhi(IrValue& inst) { } if (same == nullptr) { same = value; - } else if (!(*same == *value)) { + } else if (same != value && !(same->HasImmediate() && value->HasImmediate() && *same == *value)) { return false; } } From 942b5945d1ec288c826656e2ae391aa73ebd816c Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sun, 11 Oct 2026 19:58:37 +0300 Subject: [PATCH 071/318] fix(libkernel): leave the remaining time untouched when nanosleep completes FreeBSD's kern_clock_nanosleep writes the remaining time only when the sleep is cut short. A completed sleep zeroed rmtp, so nanosleep(&ts, &ts) in a polling loop slept zero time from the second call on. --- core/libs/prx/libkernel/Time/Time.cpp | 10 +++------- core/libs/tests/GuestNanosleep.cpp | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 581e81bb5..adf0e99e4 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -251,7 +251,7 @@ int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds) { return 0; } -static int SleepForRequest(const KernelTimespec* rqtp, KernelTimespec* rmtp) { +static int SleepForRequest(const KernelTimespec* rqtp) { constexpr int guestFault = 14; constexpr int guestInvalid = 22; if (rqtp == nullptr) return guestFault; @@ -260,20 +260,16 @@ static int SleepForRequest(const KernelTimespec* rqtp, KernelTimespec* rmtp) { CancellableSleep(static_cast(rqtp->tv_sec) * 1000000000ULL + static_cast(rqtp->tv_nsec)); } - if (rmtp != nullptr) { - rmtp->tv_sec = 0; - rmtp->tv_nsec = 0; - } return 0; } int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp) { - const int error = SleepForRequest(rqtp, rmtp); + const int error = SleepForRequest(rqtp); return error == 0 ? 0 : SceKernelError(error); } int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp) { - const int error = SleepForRequest(rqtp, rmtp); + const int error = SleepForRequest(rqtp); if (error == 0) return 0; *__error_nid_postfix() = error; return -1; diff --git a/core/libs/tests/GuestNanosleep.cpp b/core/libs/tests/GuestNanosleep.cpp index db2e4ee8b..c97b6c1f1 100644 --- a/core/libs/tests/GuestNanosleep.cpp +++ b/core/libs/tests/GuestNanosleep.cpp @@ -85,6 +85,22 @@ static void SleepsForTheRequest(Nanosleep sleep) { Require(MonotonicNanos() - start >= SLEEP_NANOS - EARLY_WAKE_MARGIN_NANOS); } +static void CompletedSleepLeavesTheRemainingTimeAlone(Nanosleep sleep) { + const KernelTimespec request{0, NANOS_PER_MILLISECOND}; + KernelTimespec remaining{7, 9}; + Require(sleep(&request, &remaining) == SCE_OK); + Require(remaining.tv_sec == 7 && remaining.tv_nsec == 9); + KernelTimespec shared = request; + for (int i = 0; i < 3; ++i) { + Require(sleep(&shared, &shared) == SCE_OK); + Require(shared.tv_sec == request.tv_sec && shared.tv_nsec == request.tv_nsec); + } + const KernelTimespec invalid{0, NANOS_PER_SECOND}; + remaining = {7, 9}; + Require(sleep(&invalid, &remaining) != SCE_OK); + Require(remaining.tv_sec == 7 && remaining.tv_nsec == 9); +} + int main() { PosixRejectsInvalidRequests(nanosleep_nid_postfix); PosixRejectsInvalidRequests(_nanosleep_nid_postfix); @@ -97,4 +113,8 @@ int main() { SleepsForTheRequest(nanosleep_nid_postfix); SleepsForTheRequest(_nanosleep_nid_postfix); SleepsForTheRequest(sceKernelNanosleep); + + CompletedSleepLeavesTheRemainingTimeAlone(nanosleep_nid_postfix); + CompletedSleepLeavesTheRemainingTimeAlone(_nanosleep_nid_postfix); + CompletedSleepLeavesTheRemainingTimeAlone(sceKernelNanosleep); } From 8db0f89e7b2650c7e4dee6bd62e78762788393ac Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sun, 11 Oct 2026 21:37:48 +0300 Subject: [PATCH 072/318] fix(libkernel): return SCE error codes from sceKernelClockGettime and sceKernelClockGetres A thread CPU clock whose thread has finished made clock_gettime fail with errno EINVAL, but the sce wrappers passed the raw -1 through to the title. They now return the encoded errno like the other sce kernel calls. --- core/libs/prx/libkernel/Time/Time.cpp | 6 ++++-- core/libs/tests/GuestThreadCpuClock.cpp | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 581e81bb5..9cd455d29 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -565,12 +565,14 @@ static constexpr int SceKernelErrorEfault = static_cast(0x8002000e); int APS5_VABI sceKernelClockGetres(KernelClockid clock_id, KernelTimespec* tp) { if (tp == nullptr) return SceKernelErrorEfault; - return clock_getres_nid_postfix(static_cast(clock_id), tp); + if (clock_getres_nid_postfix(static_cast(clock_id), tp) != 0) return SceKernelError(*__error_nid_postfix()); + return 0; } int APS5_VABI sceKernelClockGettime(KernelClockid clock_id, KernelTimespec* tp) { if (tp == nullptr) return SceKernelErrorEfault; - return clock_gettime_nid_postfix(static_cast(clock_id), tp); + if (clock_gettime_nid_postfix(static_cast(clock_id), tp) != 0) return SceKernelError(*__error_nid_postfix()); + return 0; } int APS5_VABI sceKernelConvertLocaltimeToUtc(int64_t local_time, int64_t reserved, int64_t* utc_time, KernelTimesec* timezone, int32_t* dst_seconds) { diff --git a/core/libs/tests/GuestThreadCpuClock.cpp b/core/libs/tests/GuestThreadCpuClock.cpp index 058ed0969..15946c4ae 100644 --- a/core/libs/tests/GuestThreadCpuClock.cpp +++ b/core/libs/tests/GuestThreadCpuClock.cpp @@ -12,11 +12,13 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int* APS5_VABI __error_nid_postfix(); } static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int GUEST_CLOCK_MONOTONIC = 4; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; static constexpr std::int64_t NANOS_PER_SECOND = 1000000000LL; @@ -100,6 +102,9 @@ static void OtherThreadClockReadsThatThread() { *__error_nid_postfix() = 0; Require(clock_gettime_nid_postfix(clockId, &time) == -1); Require(*__error_nid_postfix() == GUEST_EINVAL); + *__error_nid_postfix() = 0; + Require(sceKernelClockGettime(clockId, &time) == SCE_KERNEL_ERROR_EINVAL); + Require(*__error_nid_postfix() == GUEST_EINVAL); } int main() { From 87e9c70157b2a2c5cfeca11168f6a7e72bf5e4fe Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sun, 11 Oct 2026 21:38:49 +0300 Subject: [PATCH 073/318] fix(libkernel): recvfrom on a stream socket reports no sender address FreeBSD's soreceive leaves the sender unset for a stream socket, so recvfrom returns an address length of 0 (kern_recvit). The guest call formatted the empty host address as a 28-byte AF_INET6 sockaddr and reported length 28. Report length 0 when the host returns no AF_INET or AF_INET6 sender, as recvmsg does. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 ++++- core/libs/tests/GuestTcp.cpp | 25 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b5f852307..cc62c6471 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -757,7 +757,10 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if (socket->type == 1) *addressLength = 0; else UnnamedAddress(address, addressLength); return result; } - if (address) GuestAddress(native, address, addressLength); + if (address) { + if (native.ss_family == AF_INET || native.ss_family == AF_INET6) GuestAddress(native, address, addressLength); + else *addressLength = 0; + } return result; } struct GuestIovec { diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index d1f70d97d..c6fb00256 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -15,6 +15,7 @@ std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI shutdown_nid_postfix(int, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); int APS5_VABI close_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } @@ -23,7 +24,31 @@ static void Require(bool condition) { if (!condition) std::abort(); } +static void StreamReceiveReportsNoSender() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 4) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0); + const char text[] = "stream"; + Require(send_nid_postfix(client, text, sizeof(text), 0) == sizeof(text)); + std::array sender; + sender.fill(0xaa); + char received[sizeof(text)]{}; + std::uint32_t senderSize = sender.size(); + Require(recvfrom_nid_postfix(accepted, received, sizeof(received), 0, sender.data(), &senderSize) == sizeof(text)); + Require(std::strcmp(text, received) == 0); + Require(senderSize == 0 && sender[0] == 0xaa && sender[1] == 0xaa); + for (const int descriptor : {client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { + StreamReceiveReportsNoSender(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; From aaf49fb40fad6b94578b6750e3c57cf541baa9db Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sun, 11 Oct 2026 21:43:44 +0300 Subject: [PATCH 074/318] fix(libc): compare strcasecmp and strncasecmp results after lowering FreeBSD returns the difference of the lowercased bytes where the strings differ, including where one of them ends. The guest wrappers returned the raw difference of the terminator and the unfolded byte, so strcasecmp("ab", "A") gave 66 instead of 98. --- core/libs/prx/libc/src/Strings.cpp | 21 +++++++++------------ core/libs/tests/GuestStrings.cpp | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 978a499aa..0b157d0ff 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -221,24 +221,21 @@ char16_t* APS5_VABI wmemmove_nid_postfix(char16_t* dest, const char16_t* src, si extern "C" { int APS5_VABI strcasecmp_nid_postfix(const char* s1, const char* s2) { - while (*s1 && *s2) { - unsigned char a = static_cast(std::tolower(static_cast(*s1))); - unsigned char b = static_cast(std::tolower(static_cast(*s2))); - if (a != b) return a - b; - ++s1; ++s2; + for (;; ++s1, ++s2) { + const int a = std::tolower(static_cast(*s1)); + const int b = std::tolower(static_cast(*s2)); + if (a != b || *s1 == 0) return a - b; } - return static_cast(*s1) - static_cast(*s2); } int APS5_VABI strncasecmp_nid_postfix(const char* s1, const char* s2, size_t n) { - while (n && *s1 && *s2) { - unsigned char a = static_cast(std::tolower(static_cast(*s1))); - unsigned char b = static_cast(std::tolower(static_cast(*s2))); + for (; n != 0; ++s1, ++s2, --n) { + const int a = std::tolower(static_cast(*s1)); + const int b = std::tolower(static_cast(*s2)); if (a != b) return a - b; - ++s1; ++s2; --n; + if (*s1 == 0) break; } - if (!n) return 0; - return static_cast(*s1) - static_cast(*s2); + return 0; } int APS5_VABI bcmp_nid_postfix(const void* s1, const void* s2, size_t n) { diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 3f9efbb6c..01eab7dea 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -11,6 +11,8 @@ char* APS5_VABI basename_nid_postfix(const char*); char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int); char* APS5_VABI dirname_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI strcasecmp_nid_postfix(const char*, const char*); +int APS5_VABI strncasecmp_nid_postfix(const char*, const char*, std::size_t); std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); std::size_t APS5_VABI strnlen_s_nid_postfix(const char*, std::size_t); char* APS5_VABI strncat_nid_postfix(char*, const char*, std::size_t); @@ -98,6 +100,18 @@ static void CheckBoundsCheckedFunctions() { Require(snprintf_s_nid_postfix(formatted, sizeof(formatted), "%d-%s", 42, "x") == 4 && std::strcmp(formatted, "42-x") == 0); } +static void CheckCaseInsensitiveComparison() { + Require(strcasecmp_nid_postfix("", "") == 0 && strcasecmp_nid_postfix("AbC", "aBc") == 0); + Require(strcasecmp_nid_postfix("A", "b") == 'a' - 'b' && strcasecmp_nid_postfix("b", "A") == 'b' - 'a'); + Require(strcasecmp_nid_postfix("ab", "A") == 'b' && strcasecmp_nid_postfix("AB", "a") == 'b'); + Require(strcasecmp_nid_postfix("A", "aB") == -'b' && strcasecmp_nid_postfix("a", "AB") == -'b'); + Require(strcasecmp_nid_postfix("", "Z") == -'z' && strcasecmp_nid_postfix("Z", "") == 'z'); + Require(strncasecmp_nid_postfix("abc", "ABD", 2) == 0 && strncasecmp_nid_postfix("abc", "ABD", 3) == 'c' - 'd'); + Require(strncasecmp_nid_postfix("Ab", "a", 2) == 'b' && strncasecmp_nid_postfix("a", "AB", 5) == -'b'); + Require(strncasecmp_nid_postfix("Q", "", 1) == 'q' && strncasecmp_nid_postfix("", "Q", 1) == -'q'); + Require(strncasecmp_nid_postfix("x", "y", 0) == 0 && strncasecmp_nid_postfix("same", "SAME", 99) == 0); +} + static void CheckSscanfS() { #ifndef _WIN32 int number = 0; @@ -121,6 +135,7 @@ static void CheckSscanfS() { int main() { CheckBoundsCheckedFunctions(); + CheckCaseInsensitiveComparison(); CheckSscanfS(); Require(std::strcmp(dirname_nid_postfix(nullptr), ".") == 0); char emptyPath[] = ""; From 6b72296ef57c364ac25cea7f8ddc52a83013cf08 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:02:20 +0000 Subject: [PATCH 075/318] fix(libc): format 16-bit wide arguments in asprintf on Linux Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libc/src/AllocatingStrings.cpp | 45 ++------------------ core/libs/tests/Formatting.cpp | 21 ++++++++- 2 files changed, 24 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libc/src/AllocatingStrings.cpp b/core/libs/prx/libc/src/AllocatingStrings.cpp index 74779c475..2e91cc215 100644 --- a/core/libs/prx/libc/src/AllocatingStrings.cpp +++ b/core/libs/prx/libc/src/AllocatingStrings.cpp @@ -1,20 +1,15 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "SceTypes.hpp" #include #include -#include -#include #include -#include #include -#include - -#ifdef _WIN32 -#include "prx/libc/include/WindowsFormatting.hpp" -#endif extern "C" { +int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaList* args); + char* APS5_VABI strndup_nid_postfix(const char* source, std::size_t limit) { std::size_t length = 0; while (length < limit && source[length] != '\0') ++length; @@ -35,9 +30,6 @@ char* APS5_VABI strdup_nid_postfix(const char* source) { } int APS5_VABI asprintf_nid_postfix(char** destination, const char* format, ...) { - if (!destination) { errno = 22; return -1; } - *destination = nullptr; - if (!format) { errno = 22; return -1; } #ifdef _WIN32 __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -45,36 +37,7 @@ int APS5_VABI asprintf_nid_postfix(char** destination, const char* format, ...) std::va_list args; va_start(args, format); #endif - int result = -1; - try { -#ifdef _WIN32 - std::string text; - const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &text); - const char* source = text.c_str(); -#else - char* text = nullptr; - const int count = ::vasprintf(&text, format, args); - std::unique_ptr owner(text, std::free); - const char* source = text; -#endif - if (count >= 0) { - auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); - if (output) { - std::memcpy(output, source, static_cast(count) + 1); - *destination = output; - result = count; - } else errno = 12; - } - } catch (const std::bad_alloc&) { - errno = 12; - } catch (...) { -#ifdef _WIN32 - __builtin_sysv_va_end(args); -#else - va_end(args); -#endif - throw; - } + const int result = vasprintf_nid_postfix(destination, format, reinterpret_cast(args)); #ifdef _WIN32 __builtin_sysv_va_end(args); #else diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index b6134b7b9..f022c88ab 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -1,5 +1,9 @@ #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestHeap.hpp" #include +#include +#include #include #include #include @@ -15,6 +19,8 @@ int APS5_VABI vsnprintf_nid_postfix(char*, size_t, const char*, VaList*); int APS5_VABI vsnprintf_s_nid_postfix(char*, size_t, const char*, VaList*); int APS5_VABI vsscanf_s_nid_postfix(const char*, const char*, VaList*); int APS5_VABI vprintf_nid_postfix(const char*, VaList*); +int APS5_VABI asprintf_nid_postfix(char**, const char*, ...); +void APS5_VABI free_nid_postfix(void*); } static void Require(bool condition) { @@ -199,4 +205,17 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { std::puts("Formatting checks passed: 10000 iterations"); } -int main() { RunChecks(); return CheckWidePrecision() ? 0 : 1; } +static void* APS5_VABI Allocate(std::size_t bytes) { return GuestHeap::GuestHeapAllocate_nid_postfix(bytes); } +static void APS5_VABI Release(void* pointer) { GuestHeap::GuestHeapFree_nid_postfix(pointer); } +static void* APS5_VABI Unused() { std::abort(); } + +static void CheckAllocatingFormat() { + const std::array api{reinterpret_cast(&Allocate), reinterpret_cast(&Release), reinterpret_cast(&Unused), + reinterpret_cast(&Unused), reinterpret_cast(&Unused), reinterpret_cast(&Unused), reinterpret_cast(&Unused)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + char* allocated = nullptr; + Require(asprintf_nid_postfix(&allocated, "%ls:%S:%lc:%d", u"\u00e9", u"ab", 0x20ac, 7) == 11 && std::strcmp(allocated, "\xc3\xa9:ab:\xe2\x82\xac:7") == 0); + free_nid_postfix(allocated); +} + +int main() { RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } From ba9f903e3fc1affeb5331c1038e85e387feb48ab Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:10:55 +0200 Subject: [PATCH 076/318] feat(shader): implement s_subvector_loop_begin and s_subvector_loop_end --- .../tests/execution/SubvectorLoop.cpp | 209 ++++++++++++++++++ .../SpirvBackend/src/SpirvSpecialization.cpp | 8 +- .../src/ControlFlowInstructions.cpp | 35 ++- .../Translation/src/EmbeddedVertexFetch.cpp | 2 + docs/dev/TechnicalDebt.md | 1 + 5 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp new file mode 100644 index 000000000..4944237df --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp @@ -0,0 +1,209 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Columns = 9; +constexpr std::uint32_t Ones = 0xffffffffu; +constexpr std::uint32_t Marker = 0x5a5a5a5au; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ExecKeptCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, + 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, + 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, + 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, + 0xbf810000, +}; + +alignas(256) constexpr std::array ExecResetCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850002, 0xbefe04c1, + 0xbe26fff5, 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, + 0x80010a01, 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, + 0x80010e01, 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, + 0x80011201, 0xbf810000, +}; + +constexpr std::array Names{"passes", "last pass", "s38 in the pass", "exec lo in the pass", "exec hi in the pass", "s38 after the loop", "exec lo after the loop", "exec hi after the loop", "total passes"}; + +struct Group { + std::uint32_t first; + std::uint32_t last; + std::array values; +}; + +struct Case { + const char* name; + std::uint64_t mask; + std::vector groups; +}; + +const std::vector ExecKeptCases{ + {"all lanes", ~0ull, { + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 2}}, + {32, 63, {1, 2, Ones, 0, Ones, Ones, Ones, Ones, 2}}, + }}, + {"low half", 0xffffffffull, { + {0, 31, {1, 1, 0, Ones, 0, 0, Ones, 0, 1}}, + {32, 63, {0, 0, 0, 0, 0, 0, Ones, 0, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 31, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 8, 8, 0x100u, 2}}, + {3, 3, {1, 1, 0x100u, 8, 0, 8, 8, 0x100u, 2}}, + {40, 40, {1, 2, 8, 0, 0x100u, 8, 8, 0x100u, 2}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 8, 0, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 8, 0, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, 0x100u, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, 0x100u, 1}}, + }}, +}; + +const std::vector ExecResetCases{ + {"all lanes", ~0ull, { + {0, 63, {0, 0, 0, 0, 0, Ones, Ones, Ones, 1}}, + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 1}}, + }}, + {"low half", 0xffffffffull, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {0, 31, {1, 1, 0, Ones, 0, 0, 0, Ones, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 0x100u, 0x100u, Ones, 1}}, + {3, 3, {1, 1, 0x100u, 8, 0, 0x100u, 0x100u, Ones, 1}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 0, Ones, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, Ones, 1}}, + }}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t mask) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) Input[lane] = ((mask >> lane) & 1u) != 0u ? 1u : 0u; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* variant, const Case& testCase) { + std::array, Threads> expected{}; + for (const auto& group : testCase.groups) { + for (std::uint32_t lane = group.first; lane <= group.last; ++lane) expected[lane] = group.values; + } + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + const auto actual = Output[column * Threads + lane]; + Require(actual == expected[lane][column], std::string("subvector loop ") + variant + ", initial exec " + testCase.name + ": lane " + std::to_string(lane) + " " + Names[column] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[lane][column])); + } + } +} + +void CheckWave32Throws(AgcDriver::VulkanDevice& device, std::span code) { + std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(32), + {0, 0, 0, 128} + }; + request.useCache = false; + try { + static_cast(ShaderRecompiler::Recompile(request)); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("s_subvector_loop_begin in a wave32 shader is not measured") != std::string::npos, std::string("wave32 subvector loop threw an unexpected error: ") + error.what()); + return; + } + Require(false, "a wave32 subvector loop was recompiled"); +} + +void RunCases(AgcDriver::VulkanDevice& device, const char* variant, std::span code, const std::vector& cases) { + for (const auto& testCase : cases) { + Run(device, code, testCase.mask); + Check(variant, testCase); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckWave32Throws(*device, ExecKeptCode); + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + RunCases(*device, "exec kept by the body", ExecKeptCode, ExecKeptCases); + RunCases(*device, "body resets exec", ExecResetCode, ExecResetCases); + std::puts("subvector loop tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 188cae8f5..6d8d41477 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -417,6 +417,12 @@ private: } return true; }; + std::set loopHeaders; + for (const auto& [label, indices] : blocks) { + for (const auto index : indices) { + if (Opcode(instructions[index]) == spv::OpLoopMerge) loopHeaders.insert(label); + } + } std::map> edges; for (const auto& [label, indices] : blocks) { auto& terminal = instructions[indices.back()]; @@ -425,7 +431,7 @@ private: bool loop = false; for (const auto index : indices) loop |= Opcode(instructions[index]) == spv::OpLoopMerge; if (!loop && op == spv::OpBranchConditional) { - if (const auto condition = value(terminal.at(1))) target = terminal.at(*condition != 0u ? 2u : 3u); + if (const auto condition = value(terminal.at(1)); condition.has_value() && !loopHeaders.contains(terminal.at(*condition != 0u ? 3u : 2u))) target = terminal.at(*condition != 0u ? 2u : 3u); } else if (!loop && op == spv::OpSwitch) { if (const auto selector = value(terminal.at(1))) { target = terminal.at(2); diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 838346dc4..3fca25ebf 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -13,7 +13,40 @@ void TranslateControlFlowInstruction(IrBuilder& builder, const RdnaInstruction& } void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin) { - throw std::runtime_error("GNM subvector loop instructions are not supported by this translator"); + if (program.WaveSize() != 64u) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " in a wave" + std::to_string(program.WaveSize()) + " shader is not measured, only wave64 subvector loops are supported"); + } + if (inst.destination.kind != RdnaOperandKind::ScalarRegister) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " with a destination that is not an SGPR is not measured"); + } + const IrU32 execLo(ir.GetExecLo()); + const IrU32 execHi(ir.GetExecHi()); + const IrU32 counter = readRawU32(inst.destination); + const IrU32 zero(ir.Constant(0u)); + IrU32 newExecLo = execLo; + IrU32 newExecHi = execHi; + IrU32 newCounter = counter; + if (begin) { + const IrU1 lowZero(ir.IEqual(execLo.Value(), zero.Value())); + const IrU1 highZero(ir.IEqual(execHi.Value(), zero.Value())); + const IrU1 execZero(ir.LogicalAnd(lowZero.Value(), highZero.Value())); + const IrU32 entered(ir.Select(lowZero.Value(), execLo.Value(), execHi.Value())); + newCounter = IrU32(ir.Select(execZero.Value(), counter.Value(), entered.Value())); + newExecHi = IrU32(ir.Select(ir.LogicalOr(execZero.Value(), lowZero.Value()), execHi.Value(), zero.Value())); + instructionBranchCondition = execZero; + } else { + const IrU1 highSet(ir.INotEqual(execHi.Value(), zero.Value())); + const IrU1 counterZero(ir.IEqual(counter.Value(), zero.Value())); + const IrU1 stay(ir.LogicalOr(highSet.Value(), counterZero.Value())); + newExecLo = IrU32(ir.Select(highSet.Value(), counter.Value(), ir.Select(counterZero.Value(), execLo.Value(), zero.Value()))); + newExecHi = IrU32(ir.Select(stay.Value(), execHi.Value(), counter.Value())); + newCounter = IrU32(ir.Select(stay.Value(), counter.Value(), execLo.Value())); + instructionBranchCondition = IrU1(ir.LogicalNot(stay.Value())); + } + writeRawU32(inst.destination, newCounter); + ir.SetExecLo(newExecLo.Value()); + ir.SetExecHi(newExecHi.Value()); + ir.SetExec(threadBit({newExecLo, newExecHi}).Value()); } // s_*_saveexec reads its source before it writes the old exec into the destination, so diff --git a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp index 7bc692922..1e2e2d5fc 100644 --- a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp +++ b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp @@ -342,6 +342,8 @@ bool writesExec(const RdnaInstruction& inst) { case RdnaOpcode::SOrn1SaveexecB64: case RdnaOpcode::SAndn1WrexecB64: case RdnaOpcode::SAndn2WrexecB64: + case RdnaOpcode::SSubvectorLoopBegin: + case RdnaOpcode::SSubvectorLoopEnd: return true; default: return false; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index af04129b1..ac9abc9a3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -105,6 +105,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudiodecDecode](../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 - [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF - [s_sethalt](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws +- [s_subvector_loop_begin and s_subvector_loop_end](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge and the NVIDIA driver 615.78.08 crashes compiling such a loop; a loop whose back-edge block becomes unreachable in another way is not handled - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [AMPR object layout and compact gathers](../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. From c13d3b474a36c96fa6a1fb308182570dac78e4de Mon Sep 17 00:00:00 2001 From: Arekatla Nishanth Chowdary Date: Mon, 12 Oct 2026 01:12:43 +0530 Subject: [PATCH 077/318] fix(build): reject MinGW compilers that select SjLj exceptions for the libraries The libraries are built with -fno-asynchronous-unwind-tables and unwind through their own DWARF tables. MinGW-w64 GCC 11.4 and 13.2 answer that flag by switching to SjLj exceptions. Configuration succeeded and, with 13.2, the build stopped at the libc.prx link with undefined references to __gxx_personality_sj0 and _Unwind_SjLj_Register, which does not name the cause. Configuration now checks the exception model the flag selects and stops with the supported toolchain's location. --- core/libs/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2da76ed8e..453fa1829 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -26,6 +26,13 @@ target_compile_features(nid_patcher PRIVATE cxx_std_20) if(MINGW) target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++) + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_FLAGS -fno-asynchronous-unwind-tables) + check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS) + unset(CMAKE_REQUIRED_FLAGS) + if(NOT ANYPS5_MINGW_DWARF_EXCEPTIONS) + message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} selects SjLj exceptions with -fno-asynchronous-unwind-tables; the libraries need DWARF unwinding. Use the MinGW-w64 toolchain listed in docs/dev/BUILD.md.") + endif() add_compile_options(-fno-asynchronous-unwind-tables) endif() From d358374ac80f8845738da6d22b9657078eee8849 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:07:20 +0000 Subject: [PATCH 078/318] fix(libc): throw for 16-bit wide scanf conversions on Linux Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libc/src/Formatting.cpp | 15 +++++++++++++++ core/libs/tests/Formatting.cpp | 7 +++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 23 insertions(+) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index b59bbb2c8..76d5c01a1 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -35,6 +35,18 @@ bool HasGuestWideFormat(const char* format) { } return false; } + +void RejectGuestWideScan(const char* format) { + if (format == nullptr) return; + while ((format = std::strchr(format, '%')) != nullptr) { + ++format; + if (*format == '%') { ++format; continue; } + while (*format && std::strchr("*0123456789$", *format)) ++format; + if (*format == 'S' || *format == 'C' || (*format == 'l' && format[1] != '\0' && std::strchr("sc[", format[1]))) + NotImplemented_nid_no_patch("scanf 16-bit wide conversion on Linux"); + if (*format) ++format; + } +} #endif std::string HostLength(const std::string& length, char conversion) { @@ -236,6 +248,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); __builtin_sysv_va_end(args); #else + RejectGuestWideScan(format); std::va_list args; va_start(args, format); const int result = std::vfscanf(native, format, args); @@ -319,6 +332,7 @@ int APS5_VABI vsscanf_nid_postfix(const char* input, const char* format, VaList* #ifdef _WIN32 return LibcDetail::ScanWindows(input, format, args); #else + RejectGuestWideScan(format); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsscanf(input, format, copy); @@ -337,6 +351,7 @@ int APS5_VABI sscanf_nid_postfix(const char* input, const char* format, ...) { } #else int APS5_VABI sscanf_nid_postfix(const char* input, const char* format, ...) { + RejectGuestWideScan(format); std::va_list args; va_start(args, format); const int result = std::vsscanf(input, format, args); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index b6134b7b9..ca9bd4f3f 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -179,6 +179,13 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(sscanf_nid_postfix("", "%*d", &scanned) == EOF); Require(sscanf_nid_postfix(" ", "%*d%d", &scanned) == EOF); Require(sscanf_nid_postfix("", "%d", &scanned) == EOF); +#else + char16_t wide[4] = {u'!', u'!', u'!', u'!'}; + const auto rejects = [&](const char* format) { + try { sscanf_nid_postfix("ab cd", format, wide); } catch (const std::runtime_error&) { return wide[0] == u'!'; } + return false; + }; + Require(rejects("%ls") && rejects("%*s %3lc") && rejects("%l[a-z]") && rejects("%S") && rejects("%2$C")); #endif long long count = -1; int smallCount = -1; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..fa1136ccd 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -552,6 +552,7 @@ Throughout the project, every function at every stage either **does exactly what - [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); positional arguments, the `'` flag and `%m` throw when a format requires the guest formatter - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does +- [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [socketpair](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [read, write and vector I/O](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor From d5f11b156b937673f04c6ff2a782357256f7f6c6 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 16:52:04 -0300 Subject: [PATCH 079/318] fix(libSceUserService): report an unrestricted age level sceUserServiceGetAgeLevel reported 0. On a PS5 (firmware 13.60) the value is the age of the user's parental restriction (0, 7, 12 and 15 measured) and 0xFFFFFFFF without one, so 0 is a user restricted to age 0. A user who is not logged in returns 0x80960009 (not logged in) with or without an output, before the null output check. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceUserService/Export.cpp | 8 ++++++-- core/libs/prx/libSceUserService/UserService.hpp | 1 + core/libs/tests/GuestUserService.cpp | 12 ++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 2a54f7c9f..07190caa3 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -57,10 +57,14 @@ int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, int32_t } int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { - if (age_level == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + if (user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_NOT_LOGGED_IN; + } + if (age_level == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - *age_level = 0; + constexpr std::uint32_t Unrestricted = 0xFFFFFFFFu; + *age_level = Unrestricted; return USER_SERVICE_OK; } diff --git a/core/libs/prx/libSceUserService/UserService.hpp b/core/libs/prx/libSceUserService/UserService.hpp index dcc140dad..8776b8e06 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -7,6 +7,7 @@ constexpr int USER_SERVICE_OK = 0; constexpr int USER_SERVICE_ERROR_INVALID_ARGUMENT = -2137653243; constexpr int USER_SERVICE_ERROR_NO_EVENT = -2137653241; +constexpr int USER_SERVICE_ERROR_NOT_LOGGED_IN = -2137653239; constexpr int USER_SERVICE_USER_ID_INVALID = -1; constexpr int USER_SERVICE_INITIAL_USER_ID = 0x10000000; constexpr int USER_SERVICE_ERROR_BUFFER_TOO_SHORT = -2137653238; diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 823c0536a..1931f036f 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -8,6 +8,7 @@ int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list); int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id); +int APS5_VABI sceUserServiceGetAgeLevel(int user_id, std::uint32_t* age_level); } namespace { @@ -41,4 +42,15 @@ int main() { Require(sceUserServiceGetNpAccountId(123, &account_id) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID, &account_id) == USER_SERVICE_OK); Require(account_id == 0); + + std::uint32_t age_level = 0x5a5a5a5au; + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + const int absent_users[] = {0, USER_SERVICE_USER_ID_INVALID, 0xff, 0x12345678}; + for (int user_id : absent_users) { + Require(sceUserServiceGetAgeLevel(user_id, &age_level) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(age_level == 0x5a5a5a5au); + Require(sceUserServiceGetAgeLevel(user_id, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID, &age_level) == USER_SERVICE_OK); + Require(age_level == 0xffffffffu); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index af04129b1..6999b4c00 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -274,7 +274,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 cookie boxes](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` is taken to take the library context id first, as in the [prosper](https://github.com/mattias800/prosper) reimplementation; further arguments are unknown and ignored. `sceHttp2SetRequestNoContentLength` is assumed to take only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard - [sceNpSessionSignalingGetMemoryInfo](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature -- [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the meaning of the value is unknown; 0 is reported, as for the unset game presets. PPSA12544 (Unity) only stores it for its user profile +- [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile - [sceGameUpdateGetAddcontLatestVersion](../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw - [sceSystemServiceParamGetInt](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed From f40bb479e2d22c1625a9f87d9d8bc37184952dfa Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:09:33 +0000 Subject: [PATCH 080/318] fix(libkernel): report the remaining time from sceKernelWaitSema Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp | 4 +++- core/libs/tests/GuestSemaphore.cpp | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 8210cdb99..7375ae7ff 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -88,7 +88,9 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) return KERNEL_SEMA_OK; } - const bool acquired = sem->condition.WaitUntil(lock, TimedWait::DeadlineNanos(*time), released); + const std::uint64_t deadline = TimedWait::DeadlineNanos(*time); + const bool acquired = sem->condition.WaitUntil(lock, deadline, released); + *time = static_cast(TimedWait::RemainingMicros(deadline)); traceWait(!acquired); if (sem->deleted) { return SCE_KERNEL_ERROR_EACCES; diff --git a/core/libs/tests/GuestSemaphore.cpp b/core/libs/tests/GuestSemaphore.cpp index fd90d1c80..5392fa160 100644 --- a/core/libs/tests/GuestSemaphore.cpp +++ b/core/libs/tests/GuestSemaphore.cpp @@ -18,6 +18,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); +static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static void Require(bool value) { if (!value) std::abort(); } @@ -75,6 +76,12 @@ int main() { Require(sceKernelCancelSema(sem, 0, &waiters) == SCE_OK && waiters == 0); Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + KernelUseconds remaining = 1000; + Require(sceKernelWaitSema(sem, 1, &remaining) == SCE_KERNEL_ERROR_ETIMEDOUT && remaining == 0); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); + remaining = 5000000; + Require(sceKernelWaitSema(sem, 1, &remaining) == SCE_OK && remaining > 0 && remaining <= 5000000); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); Require(sceKernelWaitSema(sem, 1, nullptr) == SCE_OK); Require(sceKernelDeleteSema(sem) == SCE_OK); From e31b757c1ed3a317adc5e1f0c6c34a9da3dd6a2e Mon Sep 17 00:00:00 2001 From: Alberto Ruiz Date: Sun, 11 Oct 2026 21:13:01 +0200 Subject: [PATCH 081/318] fix(libkernel): limit param.json nesting depth parseJsonValue recursed without a limit, so a param.json with tens of thousands of nested arrays overflowed the stack. Reject nesting beyond 512 levels, the limit libSceJson2 uses. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01R5WJS4ZdQynAJesCZKH5hb --- .../AppMetadata/src/ParamJsonParser.cpp | 22 +++++++++++-------- core/libs/tests/ParamJsonParser.cpp | 18 +++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 3645c476d..d7954ebf7 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -135,9 +135,12 @@ std::string parseJsonString(const std::string& text, std::size_t& position) { return result; } -JsonValue parseJsonValue(const std::string& text, std::size_t& position); +constexpr std::size_t MaxNestingDepth = 512; -JsonValue parseJsonObject(const std::string& text, std::size_t& position) { +JsonValue parseJsonValue(const std::string& text, std::size_t& position, std::size_t depth); + +JsonValue parseJsonObject(const std::string& text, std::size_t& position, std::size_t depth) { + if (depth >= MaxNestingDepth) throw std::runtime_error("param.json is nested too deeply"); expectChar(text, position, '{'); JsonValue value; value.type = JsonType::Object; @@ -148,7 +151,7 @@ JsonValue parseJsonObject(const std::string& text, std::size_t& position) { std::string key = parseJsonString(text, position); skipWhitespace(text, position); expectChar(text, position, ':'); - JsonValue member = parseJsonValue(text, position); + JsonValue member = parseJsonValue(text, position, depth + 1); value.objectValue.emplace_back(std::move(key), std::move(member)); skipWhitespace(text, position); const char next = peekChar(text, position); @@ -159,14 +162,15 @@ JsonValue parseJsonObject(const std::string& text, std::size_t& position) { return value; } -JsonValue parseJsonArray(const std::string& text, std::size_t& position) { +JsonValue parseJsonArray(const std::string& text, std::size_t& position, std::size_t depth) { + if (depth >= MaxNestingDepth) throw std::runtime_error("param.json is nested too deeply"); expectChar(text, position, '['); JsonValue value; value.type = JsonType::Array; skipWhitespace(text, position); if (peekChar(text, position) == ']') { ++position; return value; } while (true) { - JsonValue element = parseJsonValue(text, position); + JsonValue element = parseJsonValue(text, position, depth + 1); value.arrayValue.push_back(std::move(element)); skipWhitespace(text, position); const char next = peekChar(text, position); @@ -205,11 +209,11 @@ JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { return value; } -JsonValue parseJsonValue(const std::string& text, std::size_t& position) { +JsonValue parseJsonValue(const std::string& text, std::size_t& position, std::size_t depth) { skipWhitespace(text, position); const char c = peekChar(text, position); - if (c == '{') return parseJsonObject(text, position); - if (c == '[') return parseJsonArray(text, position); + if (c == '{') return parseJsonObject(text, position, depth); + if (c == '[') return parseJsonArray(text, position, depth); if (c == '"') { JsonValue value; value.type = JsonType::String; value.stringValue = parseJsonString(text, position); return value; } if (c == 't') { expectLiteral(text, position, "true"); JsonValue value; value.type = JsonType::Boolean; value.boolValue = true; return value; } if (c == 'f') { expectLiteral(text, position, "false"); JsonValue value; value.type = JsonType::Boolean; value.boolValue = false; return value; } @@ -220,7 +224,7 @@ JsonValue parseJsonValue(const std::string& text, std::size_t& position) { JsonValue parseJsonDocument(const std::string& text) { std::size_t position = 0; - JsonValue root = parseJsonValue(text, position); + JsonValue root = parseJsonValue(text, position, 0); skipWhitespace(text, position); if (position != text.size()) throw std::runtime_error("unexpected trailing data in param.json"); return root; diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 653fb6745..22a108242 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -49,6 +49,23 @@ static void CheckUserDefinedParams(const std::filesystem::path& path) { } } +static void CheckNesting(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"extra":)"; + const auto nested = [&](std::size_t arrays) { + return prefix + std::string(arrays, '[') + std::string(arrays, ']') + "}"; + }; + Require(ParseText(path, nested(511)).title == "Example", "Nesting within the limit rejected"); + for (const std::size_t arrays : {std::size_t{512}, std::size_t{513}, std::size_t{100000}}) { + bool rejected = false; + try { + ParseText(path, nested(arrays)); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("nested too deeply") != std::string::npos; + } + Require(rejected, "Excessive nesting not rejected: " + std::to_string(arrays)); + } +} + static void CheckLanguages(const std::filesystem::path& path) { const auto nested = ParseText(path, "{\r\n \"localizedParameters\": {\r\n \"defaultLanguage\": \"en-GB\",\r\n" " \"en-GB\": { \"titleName\": \"British\" }\r\n },\r\n \"titleId\": \"PPSA00001\"\r\n}\r\n"); @@ -116,6 +133,7 @@ int main() { } CheckLanguages(path); CheckUserDefinedParams(path); + CheckNesting(path); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1; From 2d3dd337adf69fd830f7b3d514fd688b426bcae6 Mon Sep 17 00:00:00 2001 From: Jordan Hans Date: Sun, 11 Oct 2026 20:15:10 +0000 Subject: [PATCH 082/318] fix(libSceNet): accept guest socket addresses whatever their sa_len --- core/libs/prx/libSceNet/Export.cpp | 4 ++-- core/libs/tests/GuestSceNet.cpp | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 0775ad06b..92ea089fd 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -167,7 +167,7 @@ bool guest_to_native_address(const void* address, std::uint32_t length, sockaddr_storage& native, NativeLength& native_length) { if (!address || length < 2) return false; const auto* bytes = static_cast(address); - if (bytes[1] == NET_AF_INET && bytes[0] == 16 && length >= 16) { + if (bytes[1] == NET_AF_INET && length >= 16) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; std::memcpy(&v4.sin_port, bytes + 2, 2); @@ -175,7 +175,7 @@ bool guest_to_native_address(const void* address, std::uint32_t length, native_length = sizeof(v4); return true; } - if (bytes[1] == NET_AF_INET6 && bytes[0] == 28 && length >= 28) { + if (bytes[1] == NET_AF_INET6 && length >= 28) { auto& v6 = reinterpret_cast(native); v6.sin6_family = AF_INET6; std::memcpy(&v6.sin6_port, bytes + 2, 2); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e71c5d65c..9c3f9ae4b 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -295,6 +295,9 @@ static void CheckUnspecifiedIpv6() { Require(sceNetRecvfrom(receiver, received, sizeof(received), 0, peer.data(), &size) == sizeof(received)); Require(std::memcmp(received, payload, sizeof(payload)) == 0 && size == peer.size() && peer[1] == 28); Require(std::memcmp(peer.data() + 8, address.data() + 8, 16) == 0); + address[0] = 0; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + Require(sceNetRecvfrom(receiver, received, sizeof(received), 0, nullptr, nullptr) == sizeof(received)); Require(std::memcmp(sce_net_in6addr_any, unspecified.data(), unspecified.size()) == 0); Require(sceNetSocketClose(sender) == 0); Require(sceNetSocketClose(receiver) == 0); @@ -410,6 +413,10 @@ int main() { Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, source.data(), &address_size) == sizeof(datagram_result)); Require(std::strcmp(datagram, datagram_result) == 0 && source[1] == 2); + std::array unsized = address; + unsized[0] = 0; + Require(sceNetSendto(udp_sender, datagram, sizeof(datagram), 0, unsized.data(), unsized.size()) == sizeof(datagram)); + Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, nullptr, nullptr) == sizeof(datagram_result)); std::uint64_t readable[16]{}; readable[udp_receiver / 64] |= std::uint64_t{1} << (udp_receiver % 64); const std::int64_t poll_now[2]{0, 0}; From 5d298b6d950cfafdd9af5b64debb008b0c856807 Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:22:12 +0200 Subject: [PATCH 083/318] feat(libSceSysmodule): register libSceAudioPropagation module ID (0x120) --- core/libs/prx/libSceSysmodule/ModuleTable.hpp | 1 + core/libs/tests/GuestSysmodule.cpp | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index 4840450be..0d9d7f4e7 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -155,6 +155,7 @@ inline const std::unordered_map kModuleTable = { {0x00000115, "libSceNpCppWebApi"}, {0x00000116, "libSceHubAppUtil"}, {0x0000011a, "libSceNpPartner001"}, + {0x00000120, "libSceAudioPropagation"}, {0x0000012f, "libSceFontGs"}, {0x00000135, "libSceFontGsm"}, {0x00000138, "libSceNpPartnerSubscription"}, diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 4a63bbd37..be4462d41 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -147,4 +147,11 @@ int main() { Require(symbol != nullptr); Require(sceSysmoduleUnloadModule(kFiberModuleId) == 0); Require(sceSysmoduleGetModuleHandleInternal(kFiberModuleId, &handle) == kModuleUnloaded); + + constexpr std::uint16_t kAudioPropagationModuleId = 0x0120; + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleUnloadModule(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == kModuleUnloaded); } From 8e5091c2f00090cb3def109fa50d56ba3c3ff010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 22:57:06 +0200 Subject: [PATCH 084/318] docs(technical-debt): name the SPIR-V rule behind keeping the loop back edge Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ac9abc9a3..46c5768df 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -105,7 +105,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudiodecDecode](../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 - [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF - [s_sethalt](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws -- [s_subvector_loop_begin and s_subvector_loop_end](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge and the NVIDIA driver 615.78.08 crashes compiling such a loop; a loop whose back-edge block becomes unreachable in another way is not handled +- [s_subvector_loop_begin and s_subvector_loop_end](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [AMPR object layout and compact gathers](../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. From 3c581619d36358098e5b7b89fbffc2861e2450ad Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Fri, 9 Oct 2026 18:28:35 -0500 Subject: [PATCH 085/318] feat(input): configure stick deadzones and mouse sensitivity anyps5-input.ini accepts LeftStickDeadzone and RightStickDeadzone (whole percent, 0 to 90), a radial deadzone on the controller sticks whose remaining travel is rescaled to the full range, and MouseSensitivity (above 0, at most 10), which replaces the fixed factor of 1 for mouse look. The defaults keep the current mapping exactly. --- .../prx/libScePad/include/InputMapping.hpp | 18 ++- core/libs/prx/libSceVideoOut/CMakeLists.txt | 10 ++ .../prx/libSceVideoOut/include/PadInput.hpp | 2 + .../prx/libSceVideoOut/src/InputMapping.cpp | 95 +++++++++++++- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 24 ++-- .../prx/libSceVideoOut/tests/InputMapping.cpp | 119 ++++++++++++++++++ docs/user/INPUT_MAPPING.md | 13 ++ 7 files changed, 263 insertions(+), 18 deletions(-) create mode 100644 core/libs/prx/libSceVideoOut/tests/InputMapping.cpp diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index ea3513278..21cccc0ae 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -13,7 +13,20 @@ namespace Pad { inline constexpr int MousePollIntervalMs = 33; inline constexpr int WheelPressDurationMs = 80; -inline constexpr double MouseSensitivity = 1.0; +inline constexpr double DefaultMouseSensitivity = 1.0; +inline constexpr double MaxMouseSensitivity = 10.0; +inline constexpr int MaxStickDeadzonePercent = 90; + +struct InputSettings { + int leftStickDeadzonePercent = 0; + int rightStickDeadzonePercent = 0; + double mouseSensitivity = DefaultMouseSensitivity; +}; + +struct InputConfiguration { + std::vector bindings; + InputSettings settings; +}; inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_F11, MouseButton::None, InputControl::ToggleFullscreen}, @@ -51,7 +64,8 @@ inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::None, InputControl::Button, PadButton::Down, -1} }; -std::vector LoadInputMapping(); +InputConfiguration LoadInputMapping(); +std::array StickWithDeadzone(std::int16_t x, std::int16_t y, int deadzonePercent); } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 3b7bb08b9..8c888a493 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -52,6 +52,16 @@ if(BUILD_TESTING) add_test_executable(pad_sdl_tests tests/PadSdl.cpp) target_link_libraries(pad_sdl_tests PRIVATE SDL2-static) add_test(NAME pad_sdl COMMAND pad_sdl_tests) + add_test_executable(pad_input_mapping_tests tests/InputMapping.cpp src/InputMapping.cpp) + target_include_directories(pad_input_mapping_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_link_libraries(pad_input_mapping_tests PRIVATE SDL2-static libc) + set_target_properties(pad_input_mapping_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(pad_input_mapping_tests) + add_test(NAME pad_input_mapping COMMAND pad_input_mapping_tests) + if(WIN32) + set_tests_properties(pad_input_mapping PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index f0cbea89c..ce5188877 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -19,6 +19,7 @@ public: void Update(); private: + explicit PadInput(Pad::InputConfiguration configuration); void publish(); void setMouseMode(bool enabled); void openFirstAvailableController(); @@ -29,6 +30,7 @@ private: void enableSensors(); std::vector bindings; + Pad::InputSettings settings; std::vector pressed; std::vector wheelReleaseTimes; std::array mouseStick{128, 128}; diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index d3d3d7275..b407bfeb2 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -121,6 +123,63 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc throw std::runtime_error("source type must be KEY, MOUSE or WHEEL"); } +enum class InputSetting { + LeftStickDeadzone, + RightStickDeadzone, + MouseSensitivity +}; + +struct InputSettingName { + std::string_view name; + InputSetting setting; +}; + +constexpr auto settingNames = std::array{ + InputSettingName{"LeftStickDeadzone", InputSetting::LeftStickDeadzone}, + InputSettingName{"RightStickDeadzone", InputSetting::RightStickDeadzone}, + InputSettingName{"MouseSensitivity", InputSetting::MouseSensitivity} +}; + +const InputSettingName* findSetting(std::string_view name) { + const auto normalized = upper(name); + for (const auto& setting : settingNames) { + if (upper(setting.name) == normalized) return &setting; + } + return nullptr; +} + +int parseDeadzonePercent(std::string_view value) { + int parsed = 0; + const auto end = value.data() + value.size(); + const auto result = std::from_chars(value.data(), end, parsed); + if (result.ec != std::errc{} || result.ptr != end || parsed < 0 || parsed > Pad::MaxStickDeadzonePercent) { + throw std::runtime_error("deadzone must be a whole percentage from 0 to " + std::to_string(Pad::MaxStickDeadzonePercent)); + } + return parsed; +} + +double parseMouseSensitivity(std::string_view value) { + double parsed = 0.0; + const auto end = value.data() + value.size(); + const auto result = std::from_chars(value.data(), end, parsed); + if (result.ec != std::errc{} || result.ptr != end || !(parsed > 0.0) || parsed > Pad::MaxMouseSensitivity) { + throw std::runtime_error("mouse sensitivity must be a number greater than 0 and at most 10"); + } + return parsed; +} + +void applySetting(Pad::InputSettings& settings, InputSetting setting, std::string_view value) { + switch (setting) { + case InputSetting::LeftStickDeadzone: settings.leftStickDeadzonePercent = parseDeadzonePercent(value); break; + case InputSetting::RightStickDeadzone: settings.rightStickDeadzonePercent = parseDeadzonePercent(value); break; + case InputSetting::MouseSensitivity: settings.mouseSensitivity = parseMouseSensitivity(value); break; + } +} + +std::uint8_t stickByte(std::int32_t axis) { + return static_cast(((axis + 32768) * 255 + 32767) / 65535); +} + [[noreturn]] void invalidLine(const std::filesystem::path& path, std::size_t line, const std::string& reason) { throw std::runtime_error("Pad: invalid input mapping " + path.string() + ":" + std::to_string(line) + ": " + reason); } @@ -133,8 +192,24 @@ std::filesystem::path defaultConfigPath() { } -std::vector Pad::LoadInputMapping() { - std::vector bindings(InputMapping.begin(), InputMapping.end()); +std::array Pad::StickWithDeadzone(std::int16_t x, std::int16_t y, int deadzonePercent) { + if (deadzonePercent < 0 || deadzonePercent > MaxStickDeadzonePercent) throw std::invalid_argument("Pad: stick deadzone out of range"); + if (deadzonePercent == 0) return {stickByte(x), stickByte(y)}; + const double normalizedX = std::max(x / 32767.0, -1.0); + const double normalizedY = std::max(y / 32767.0, -1.0); + const double magnitude = std::hypot(normalizedX, normalizedY); + const double deadzone = deadzonePercent / 100.0; + if (magnitude <= deadzone) return {stickByte(0), stickByte(0)}; + const double scale = (magnitude - deadzone) / (1.0 - deadzone) / magnitude; + const auto axis = [scale](double value) { + return stickByte(static_cast(std::lround(std::clamp(value * scale, -1.0, 1.0) * 32767.0))); + }; + return {axis(normalizedX), axis(normalizedY)}; +} + +Pad::InputConfiguration Pad::LoadInputMapping() { + InputConfiguration configuration{{InputMapping.begin(), InputMapping.end()}, {}}; + auto& bindings = configuration.bindings; const char* configuredPath = std::getenv("ANYPS5_INPUT_CONFIG"); const bool explicitPath = configuredPath != nullptr && configuredPath[0] != '\0'; const std::filesystem::path path = explicitPath ? configuredPath : defaultConfigPath(); @@ -143,10 +218,11 @@ std::vector Pad::LoadInputMapping() { if (explicitPath || std::filesystem::exists(path)) { throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); } - return bindings; + return configuration; } std::unordered_set overriddenActions; + std::array configuredSettings{}; std::string line; std::size_t lineNumber = 0; while (std::getline(file, line)) { @@ -160,6 +236,17 @@ std::vector Pad::LoadInputMapping() { const auto actionName = trim(content.substr(0, separator)); const auto source = trim(content.substr(separator + 1)); + if (const auto* setting = findSetting(actionName)) { + auto& configured = configuredSettings[static_cast(setting->setting)]; + if (configured) invalidLine(path, lineNumber, std::string(setting->name) + " is set more than once"); + configured = true; + try { + applySetting(configuration.settings, setting->setting, source); + } catch (const std::runtime_error& error) { + invalidLine(path, lineNumber, error.what()); + } + continue; + } const auto* action = findAction(actionName); if (action == nullptr) invalidLine(path, lineNumber, "unknown action '" + std::string(actionName) + "'"); if (source.empty()) invalidLine(path, lineNumber, "source is empty"); @@ -182,5 +269,5 @@ std::vector Pad::LoadInputMapping() { } if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); - return bindings; + return configuration; } diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index ab3aa11a8..5e5f7690a 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "SDL.h" #include "prx/libSceVideoOut/include/PadInput.hpp" @@ -13,8 +14,11 @@ #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" -PadInput::PadInput() - : bindings(Pad::LoadInputMapping()), pressed(bindings.size()), wheelReleaseTimes(bindings.size()) { +PadInput::PadInput() : PadInput(Pad::LoadInputMapping()) { +} + +PadInput::PadInput(Pad::InputConfiguration configuration) + : bindings(std::move(configuration.bindings)), settings(configuration.settings), pressed(bindings.size()), wheelReleaseTimes(bindings.size()) { openFirstAvailableController(); } @@ -153,16 +157,12 @@ PadInputState PadInput::sampleController() const { if (result.analogButtonsL2 != 0) result.buttons |= static_cast(Pad::PadButton::L2); if (result.analogButtonsR2 != 0) result.buttons |= static_cast(Pad::PadButton::R2); - const auto stickValue = [this](SDL_GameControllerAxis axis) { - const auto value = static_cast(SDL_GameControllerGetAxis(controller, axis)) + 32768; - return static_cast((value * 255 + 32767) / 65535); - }; - result.sticks = { - stickValue(SDL_CONTROLLER_AXIS_LEFTX), - stickValue(SDL_CONTROLLER_AXIS_LEFTY), - stickValue(SDL_CONTROLLER_AXIS_RIGHTX), - stickValue(SDL_CONTROLLER_AXIS_RIGHTY) + const auto stick = [this](SDL_GameControllerAxis xAxis, SDL_GameControllerAxis yAxis, int deadzonePercent) { + return Pad::StickWithDeadzone(SDL_GameControllerGetAxis(controller, xAxis), SDL_GameControllerGetAxis(controller, yAxis), deadzonePercent); }; + const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, settings.leftStickDeadzonePercent); + const auto right = stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, settings.rightStickDeadzonePercent); + result.sticks = {left[0], left[1], right[0], right[1]}; switch (SDL_GameControllerGetType(controller)) { case SDL_CONTROLLER_TYPE_PS5: result.deviceKind = 1; break; case SDL_CONTROLLER_TYPE_PS4: result.deviceKind = 2; break; @@ -292,7 +292,7 @@ void PadInput::Update() { mouseStick = {128, 128}; if (deltaX != 0 || deltaY != 0) { const double distance = std::hypot(deltaX, deltaY); - const double scale = std::clamp(distance * Pad::MouseSensitivity + 16.0, 64.0, 128.0) / distance; + const double scale = std::clamp(distance * settings.mouseSensitivity + 16.0, 64.0, 128.0) / distance; const auto mapAxis = [scale](int delta) { return static_cast(std::clamp(128L + std::lround(delta * scale), 0L, 255L)); }; mouseStick = {mapAxis(deltaX), mapAxis(deltaY)}; } diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp new file mode 100644 index 000000000..0a67672ea --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -0,0 +1,119 @@ +#include "prx/libScePad/include/InputMapping.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::filesystem::path root; + +void useConfig(const std::string& text) { + const auto path = root / "anyps5-input.ini"; + { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + file << text; + require(file.good(), "write the input mapping"); + } +#ifdef _WIN32 + require(_putenv_s("ANYPS5_INPUT_CONFIG", path.string().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); +#else + require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); +#endif +} + +bool rejects(const std::string& text) { + useConfig(text); + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +std::uint8_t plainStickByte(std::int16_t axis) { + return static_cast(((static_cast(axis) + 32768) * 255 + 32767) / 65535); +} + +void settingsDefault() { + useConfig("Cross = KEY:F\n"); + const auto configuration = Pad::LoadInputMapping(); + require(configuration.settings.leftStickDeadzonePercent == 0 && configuration.settings.rightStickDeadzonePercent == 0, "deadzones default to 0"); + require(configuration.settings.mouseSensitivity == Pad::DefaultMouseSensitivity, "mouse sensitivity defaults to 1"); +} + +void settingsParse() { + useConfig("leftstickdeadzone = 12 ; worn stick\nRightStickDeadzone=5\nMouseSensitivity = 2.5\nCross = KEY:F\n"); + const auto configuration = Pad::LoadInputMapping(); + require(configuration.settings.leftStickDeadzonePercent == 12, "left deadzone is read case-insensitively"); + require(configuration.settings.rightStickDeadzonePercent == 5, "right deadzone is read"); + require(configuration.settings.mouseSensitivity == 2.5, "mouse sensitivity is read"); + bool crossOnF = false; + for (const auto& binding : configuration.bindings) crossOnF = crossOnF || (binding.control == Pad::InputControl::Button && binding.button == Pad::PadButton::Cross && binding.key == SDL_SCANCODE_F); + require(crossOnF, "bindings are read alongside settings"); +} + +void settingsReject() { + require(rejects("LeftStickDeadzone = 91\n"), "a deadzone above 90 is rejected"); + require(rejects("LeftStickDeadzone = -1\n"), "a negative deadzone is rejected"); + require(rejects("LeftStickDeadzone = 7.5\n"), "a fractional deadzone is rejected"); + require(rejects("RightStickDeadzone =\n"), "an empty deadzone is rejected"); + require(rejects("MouseSensitivity = 0\n"), "a zero mouse sensitivity is rejected"); + require(rejects("MouseSensitivity = 10.5\n"), "a mouse sensitivity above 10 is rejected"); + require(rejects("MouseSensitivity = fast\n"), "a non-numeric mouse sensitivity is rejected"); + require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected"); +} + +void deadzoneOffMatchesRawSticks() { + for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) { + const auto stick = Pad::StickWithDeadzone(axis, axis, 0); + require(stick[0] == plainStickByte(axis) && stick[1] == plainStickByte(axis), "a 0 deadzone keeps the raw mapping"); + } +} + +void deadzoneShapesSticks() { + require(Pad::StickWithDeadzone(3000, -3000, 15) == std::array{128, 128}, "a small deflection inside the deadzone is centered"); + require(Pad::StickWithDeadzone(32767, 0, 15) == std::array{255, 128}, "a full deflection still reaches the edge"); + require(Pad::StickWithDeadzone(-32768, 0, 15) == std::array{0, 128}, "a full negative deflection still reaches the edge"); + require(Pad::StickWithDeadzone(32767, 32767, 15) == std::array{255, 255}, "a full diagonal still reaches the corner"); + const auto justOutside = Pad::StickWithDeadzone(5100, 0, 15); + require(justOutside[0] >= 128 && justOutside[0] <= 130 && justOutside[1] == 128, "the output starts at the center just outside the deadzone"); + const auto half = Pad::StickWithDeadzone(16384, 0, 20); + require(half[0] > 128 && half[0] < plainStickByte(16384), "a deflection outside the deadzone is rescaled"); + bool rejected = false; + try { + Pad::StickWithDeadzone(0, 0, 91); + } catch (const std::invalid_argument&) { + rejected = true; + } + require(rejected, "an out-of-range deadzone is rejected"); +} + +} + +int main() try { + root = std::filesystem::temp_directory_path() / ("anyps5_input_mapping-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + settingsDefault(); + settingsParse(); + settingsReject(); + deadzoneOffMatchesRawSticks(); + deadzoneShapesSticks(); + std::filesystem::remove_all(root); + std::cout << "input mapping tests passed\n"; + return 0; +} catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; +} diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 56509963d..83876a67d 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -30,4 +30,17 @@ R2 = MOUSE:Right ToggleMouse = MOUSE:Middle ``` +The same file also accepts these settings, each at most once, in the form `Setting = Value`: + +| Setting | Value | Default | +|---------|-------|---------| +| `LeftStickDeadzone`, `RightStickDeadzone` | Whole percentage from 0 to 90 of the controller stick's travel that reads as centered. Outside it the travel is rescaled, so the stick still reaches its full range. Keyboard and mouse stick bindings are not affected. | `0` | +| `MouseSensitivity` | Number greater than 0 and at most 10 that scales mouse movement on the right stick while the mouse is captured (`ToggleMouse`). | `1` | + +```ini +LeftStickDeadzone = 12 +RightStickDeadzone = 8 +MouseSensitivity = 2.5 +``` + An invalid line reports the file and line number and stops input initialization. If the configured file does not exist or cannot be read, AnyPS5 reports an error. With no `anyps5-input.ini` and no `ANYPS5_INPUT_CONFIG`, the built-in mapping is used. From 602a76fc26f9ddf755a61fadc5840d3d8dda07c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:57:22 +0000 Subject: [PATCH 086/318] fix(libkernel): let pthread_barrier_destroy wait for released waiters Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- .../libs/prx/libkernel/Pthread/Posix/Barrier.cpp | 7 ++++--- core/libs/tests/GuestPthreadBarrier.cpp | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp index 140b6900e..4d03696ff 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -86,7 +86,7 @@ int APS5_VABI pthread_barrier_wait_nid_postfix(PthreadBarrierPrivate** barrier) return -1; } value.changed.wait(lock, [&] { return value.generation != generation; }); - --value.active; + if (--value.active == 0) value.changed.notify_all(); return 0; } @@ -94,8 +94,9 @@ int APS5_VABI pthread_barrier_destroy_nid_postfix(PthreadBarrierPrivate** barrie if (!barrier || !*barrier) return 22; auto* value = *barrier; { - std::lock_guard lock(value->mutex); - if (value->active != 0) return 16; + std::unique_lock lock(value->mutex); + if (value->arrived != 0) return 16; + value->changed.wait(lock, [&] { return value->active == 0; }); } delete value; *barrier = nullptr; diff --git a/core/libs/tests/GuestPthreadBarrier.cpp b/core/libs/tests/GuestPthreadBarrier.cpp index 64d67e201..6686ebc01 100644 --- a/core/libs/tests/GuestPthreadBarrier.cpp +++ b/core/libs/tests/GuestPthreadBarrier.cpp @@ -42,6 +42,13 @@ static void* APS5_VABI Waiter(void* arg) { return nullptr; } +static void* APS5_VABI DestroyingWaiter(void* arg) { + auto& context = *static_cast(arg); + if (pthread_barrier_wait_nid_postfix(&context.barrier) == BARRIER_SERIAL_THREAD) + context.serial = pthread_barrier_destroy_nid_postfix(&context.barrier) == 0 && context.barrier == nullptr ? 1 : -1; + return nullptr; +} + static void RunWaiters(Context& context) { std::array threads{}; for (auto& thread : threads) Require(scePthreadCreate(&thread, nullptr, Waiter, &context, nullptr) == 0); @@ -101,6 +108,15 @@ int main() { RunWaiters(withoutAttr); Require(pthread_barrier_destroy_nid_postfix(&withoutAttr.barrier) == 0); + for (int round = 0; round < 50; ++round) { + Context destroyed; + Require(pthread_barrier_init_nid_postfix(&destroyed.barrier, nullptr, WAITERS) == 0); + std::array threads{}; + for (auto& thread : threads) Require(scePthreadCreate(&thread, nullptr, DestroyingWaiter, &destroyed, nullptr) == 0); + for (auto& thread : threads) Require(scePthreadJoin(thread, nullptr) == 0); + Require(destroyed.serial == 1); + } + GuestBarrier* single = nullptr; Require(pthread_barrierattr_init_nid_postfix(&attr) == 0); Require(pthread_barrier_init_nid_postfix(&single, &attr, 1) == 0); From 29af23e718f0ab9c45e4d34bf803899d00c8322b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:58:50 +0000 Subject: [PATCH 087/318] fix(libkernel): apply the memory type in sceKernelMemoryPoolBatch type-protect entries Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- .../prx/libkernel/DirectMemory/MemoryPoolExport.cpp | 2 +- core/libs/tests/GuestMemory.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp index 27076063a..0aed1836e 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp @@ -70,7 +70,7 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry* entries case 1: return PoolCommit(e.commit.addr, e.commit.len, e.commit.prot); case 2: return PoolDecommit(e.decommit.addr, e.decommit.len); case 3: return DoMprotect(e.protect.addr, static_cast(e.protect.len), e.protect.prot); - case 4: return DoMprotect(e.type_protect.addr, static_cast(e.type_protect.len), e.type_protect.prot); + case 4: return DoMtypeprotect(e.type_protect.addr, static_cast(e.type_protect.len), e.type_protect.type, e.type_protect.prot); default: return SCE_KERNEL_ERROR_EINVAL; } }); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index bfab22098..478c5c37c 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -53,6 +53,7 @@ int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); int APS5_VABI sceKernelMtypeprotect(const void*, std::size_t, int, int); int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry*, int, int*, int); +int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry*, int, int*, int); int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const char*); int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); @@ -421,6 +422,16 @@ static void CheckMtypeprotect() { check(0, 0, 3); check(1, 3, 1); check(2, 5, 3); + KernelMemoryPoolBatchEntry poolEntry{}; + poolEntry.op = 4; + poolEntry.type_protect.addr = bytes + page * 2; + poolEntry.type_protect.len = page; + poolEntry.type_protect.prot = 1; + poolEntry.type_protect.type = 4; + processed = -1; + Require(sceKernelMemoryPoolBatch(&poolEntry, 1, &processed, 0) == 0 && processed == 1); + check(1, 3, 1); + check(2, 4, 1); Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); VirtualQueryInfo whole{}; Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); From 74d44b8e6c76cf2e57832a6df92e01b27342f08d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 21:00:30 +0000 Subject: [PATCH 088/318] fix(libkernel): try the lock before checking the pthread_mutex_timedlock deadline Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- .../prx/libkernel/Pthread/Posix/Mutex.cpp | 11 ++++----- core/libs/tests/GuestPthreadMutexattr.cpp | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index ee0695d43..7ab97118f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -6,14 +6,13 @@ #include "../include/Mutex.hpp" #include "Common.hpp" #include -#include -#include #include #include namespace { constexpr int POSIX_EINVAL = 22; +constexpr int GUEST_REALTIME_CLOCK = 0; constexpr int POSIX_PRIO_PROTECT = 2; constexpr int POSIX_SCHED_RR = 3; constexpr std::uintptr_t POSIX_ADAPTIVE_MUTEX_INITIALIZER = 1; @@ -62,12 +61,10 @@ int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex) { int APS5_VABI pthread_mutex_timedlock_nid_postfix(PthreadMutex* mutex, const KernelTimespec* abstime) { if (!abstime) throw std::runtime_error("pthread_mutex_timedlock: null abstime"); - if (abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return POSIX_EINVAL; _initializeStatic(mutex, __func__); - const auto deadline = std::chrono::seconds(abstime->tv_sec) + std::chrono::nanoseconds(abstime->tv_nsec); - const auto now = std::chrono::system_clock::now().time_since_epoch(); - const auto remaining = std::chrono::duration_cast(deadline - now).count(); - const auto usec = remaining <= 0 ? 0 : remaining >= std::numeric_limits::max() ? std::numeric_limits::max() : static_cast(remaining); + if (scePthreadMutexTrylock(mutex) == 0) return 0; + KernelUseconds usec = 0; + if (!PosixThread::RelativeMicroseconds(GUEST_REALTIME_CLOCK, abstime, &usec)) return POSIX_EINVAL; return PosixThread::ToErrno(scePthreadMutexTimedlock(mutex, usec)); } diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 410fb354e..410aa02ba 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -1,4 +1,5 @@ #include "SceTypes.hpp" +#include #include #include @@ -14,6 +15,9 @@ int APS5_VABI pthread_mutex_init_nid_postfix(PthreadMutex* mutex, const PthreadM int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex); int APS5_VABI pthread_mutex_unlock_nid_postfix(PthreadMutex* mutex); int APS5_VABI pthread_mutex_destroy_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_mutex_timedlock_nid_postfix(PthreadMutex* mutex, const KernelTimespec* abstime); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI sched_get_priority_max_nid_postfix(int policy); } @@ -35,6 +39,25 @@ static bool Throws(Call call) { return false; } +static constexpr KernelTimespec INVALID_DEADLINE{0, 1000000000}; + +static void* APS5_VABI TimedlockInvalidDeadline(void* arg) { + auto* mutex = static_cast(arg); + return reinterpret_cast(static_cast(pthread_mutex_timedlock_nid_postfix(mutex, &INVALID_DEADLINE))); +} + +static void CheckTimedlockInvalidDeadline() { + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, nullptr) == 0); + Require(pthread_mutex_timedlock_nid_postfix(&mutex, &INVALID_DEADLINE) == 0); + Pthread thread{}; + void* result = nullptr; + Require(scePthreadCreate(&thread, nullptr, TimedlockInvalidDeadline, &mutex, nullptr) == 0); + Require(scePthreadJoin(thread, &result) == 0 && reinterpret_cast(result) == GUEST_EINVAL); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); +} + static void CheckType() { PthreadMutexattr attr = nullptr; Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); @@ -55,6 +78,7 @@ static void CheckType() { int main() { CheckType(); + CheckTimedlockInvalidDeadline(); PthreadMutexattr attr = nullptr; Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); int ceiling = -1; From e463c5d11fc5dac73887f17031cf36d9619730c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 21:01:57 +0000 Subject: [PATCH 089/318] fix(libkernel): clamp POSIX deadlines beyond the clock range instead of overflowing Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libkernel/Pthread/Posix/Common.hpp | 16 ++++++++++++---- core/libs/tests/GuestPthreadRwlock.cpp | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Common.hpp b/core/libs/prx/libkernel/Pthread/Posix/Common.hpp index cf95f707e..226521648 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Common.hpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Common.hpp @@ -24,10 +24,18 @@ inline bool RelativeMicroseconds(int clockId, const KernelTimespec* abstime, Ker if (!abstime || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return false; KernelTimespec now{}; clock_gettime_nid_postfix(clockId, &now); - const auto deadline = std::chrono::seconds(abstime->tv_sec) + std::chrono::nanoseconds(abstime->tv_nsec); - const auto current = std::chrono::seconds(now.tv_sec) + std::chrono::nanoseconds(now.tv_nsec); - const auto remaining = std::chrono::duration_cast(deadline - current).count(); - *usec = remaining <= 0 ? 0 : remaining >= std::numeric_limits::max() ? std::numeric_limits::max() : static_cast(remaining); + constexpr auto maximum = std::numeric_limits::max(); + if (abstime->tv_sec < now.tv_sec - 1) { + *usec = 0; + return true; + } + const std::int64_t seconds = abstime->tv_sec - now.tv_sec; + if (seconds > static_cast(maximum / 1000000) + 1) { + *usec = maximum; + return true; + } + const std::int64_t remaining = (seconds * 1000000000 + abstime->tv_nsec - now.tv_nsec) / 1000; + *usec = remaining <= 0 ? 0 : remaining >= maximum ? maximum : static_cast(remaining); return true; } diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index ffae6559a..223de9030 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -190,8 +190,23 @@ static void CheckFailedLockAttempts() { Require(rwlock == nullptr); } +static void CheckDeadlinesBeyondTheClockRange() { + PthreadRwlock rwlock = nullptr; + Holder reader{&rwlock, false}; + Start(reader); + const KernelTimespec farPast{INT64_MIN, 0}; + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &farPast) == GUEST_ETIMEDOUT); + reader.release.store(true); + const KernelTimespec farFuture{INT64_MAX, 0}; + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &farFuture) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); +} + int main() { CheckConcurrentFirstWrlockExcludes(); + CheckDeadlinesBeyondTheClockRange(); const KernelTimespec invalid{0, NANOS_PER_SECOND}; PthreadRwlock fresh[4] = {}; Require(pthread_rwlock_tryrdlock_nid_postfix(&fresh[0]) == 0); From fe4a2cf7d803eee36a2108c04e3ab7b097a8f8ea Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 21:05:51 +0000 Subject: [PATCH 090/318] fix(libc): run atexit handlers in one order with __cxa_atexit destructors Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libc/src/Process.cpp | 20 +++++--------------- core/libs/tests/GuestExitOrder.cpp | 17 ++++++++++++----- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 0e2341b02..fd810843e 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -18,6 +18,8 @@ #include "prx/libc/include/General.hpp" #include "SceTypes.hpp" +extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle); + namespace { std::mutex shutdownMutex; @@ -31,17 +33,9 @@ std::stop_source shutdownSource; std::atomic exitRequested{false}; std::atomic exitStarted{false}; using GuestExitCallback = void (APS5_VABI*)(); -std::mutex exitCallbackMutex; -std::vector exitCallbacks; -void runGuestExitCallback() { - GuestExitCallback callback; - { - std::lock_guard lock(exitCallbackMutex); - callback = exitCallbacks.back(); - exitCallbacks.pop_back(); - } - callback(); +void APS5_VABI RunGuestExitCallback(void* callback) { + reinterpret_cast(callback)(); } } @@ -166,11 +160,7 @@ int* APS5_VABI __error_nid_postfix() { int APS5_VABI atexit_nid_postfix(GuestExitCallback func) { if (func == nullptr) return 0; - std::lock_guard lock(exitCallbackMutex); - exitCallbacks.push_back(func); - const int result = std::atexit(runGuestExitCallback); - if (result != 0) exitCallbacks.pop_back(); - return result; + return __cxa_atexit_nid_postfix(RunGuestExitCallback, reinterpret_cast(func), nullptr); } namespace { diff --git a/core/libs/tests/GuestExitOrder.cpp b/core/libs/tests/GuestExitOrder.cpp index 067bea90f..889e6f0f2 100644 --- a/core/libs/tests/GuestExitOrder.cpp +++ b/core/libs/tests/GuestExitOrder.cpp @@ -6,6 +6,7 @@ #include extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle); +extern "C" int APS5_VABI atexit_nid_postfix(void (APS5_VABI *func)()); namespace { @@ -19,14 +20,16 @@ void APS5_VABI GuestHandler(void*) { Record('G'); } void HostHandler() { Record('H'); } -bool GuestHandlersRunFirst() { +void APS5_VABI AtexitHandler() { Record('A'); } + +template +bool ExitOrder(Setup setup, const char* expected) { if (pipe(events) != 0) return false; const pid_t child = fork(); if (child < 0) return false; if (child == 0) { close(events[0]); - __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); - std::atexit(HostHandler); + setup(); LibcExit_nid_no_patch(0); } close(events[1]); @@ -39,15 +42,19 @@ bool GuestHandlersRunFirst() { } close(events[0]); int status = 0; - return waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0 && length == 2 && order[0] == 'G' && order[1] == 'H'; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0 && length == 2 && order[0] == expected[0] && order[1] == expected[1]; } } int main() { - if (!GuestHandlersRunFirst()) { + if (!ExitOrder([] { __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); std::atexit(HostHandler); }, "GH")) { std::fputs("guest exit handlers must run before the host exit handlers\n", stderr); return 1; } + if (!ExitOrder([] { __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); atexit_nid_postfix(AtexitHandler); }, "AG")) { + std::fputs("atexit and __cxa_atexit handlers must run in reverse order of registration\n", stderr); + return 1; + } return 0; } From 14894001893e851d58e764a805dbccaed5fa0ae8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 21:06:35 +0000 Subject: [PATCH 091/318] fix(libc): set EINVAL when getenv gets an invalid name Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libc/src/Environment.cpp | 2 +- core/libs/tests/GuestEnvironment.cpp | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp index 080c8b110..1cdc86a2f 100644 --- a/core/libs/prx/libc/src/Environment.cpp +++ b/core/libs/prx/libc/src/Environment.cpp @@ -49,7 +49,7 @@ int EnvironmentError(int error) { errno = error; return -1; } extern "C" { char* APS5_VABI getenv_nid_postfix(const char* name) { - if (!ValidEnvironmentName(name)) return nullptr; + if (!ValidEnvironmentName(name)) { errno = 22; return nullptr; } try { std::lock_guard lock(environmentMutex); for (auto& entry : Environment()) diff --git a/core/libs/tests/GuestEnvironment.cpp b/core/libs/tests/GuestEnvironment.cpp index df1910cf4..368dc4097 100644 --- a/core/libs/tests/GuestEnvironment.cpp +++ b/core/libs/tests/GuestEnvironment.cpp @@ -36,6 +36,13 @@ int main() { Require(std::strcmp(getenv_nid_postfix(key), "two") == 0); Require(setenv_nid_postfix("bad=name", "x", 1) == -1); Require(*__error_nid_postfix() == 22); + const char* const invalidNames[] = {nullptr, "", "bad=name"}; + for (const char* name : invalidNames) { + *__error_nid_postfix() = 0; + Require(getenv_nid_postfix(name) == nullptr && *__error_nid_postfix() == 22); + } + *__error_nid_postfix() = 34; + Require(getenv_nid_postfix("ANYPS5_GUEST_ENV_ABSENT_4C27") == nullptr && *__error_nid_postfix() == 34); Require(unsetenv_nid_postfix("") == -1); char invalid[] = "no-separator"; Require(putenv_nid_postfix(invalid) == -1); From 11fa3ab843686a4bc1cfc496f5ebb1c99ee6b80d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 21:07:20 +0000 Subject: [PATCH 092/318] fix(libc): keep getenv results valid when other variables change Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libc/src/Environment.cpp | 11 ++++++----- core/libs/tests/GuestEnvironment.cpp | 8 ++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp index 080c8b110..0168ac6c9 100644 --- a/core/libs/prx/libc/src/Environment.cpp +++ b/core/libs/prx/libc/src/Environment.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -14,9 +15,9 @@ extern char** environ; namespace { struct EnvironmentEntry { - std::string owned; + std::unique_ptr owned; char* borrowed = nullptr; - char* Data() { return borrowed ? borrowed : owned.data(); } + char* Data() { return borrowed ? borrowed : owned->data(); } bool Matches(std::string_view name) { const char* data = Data(); const char* separator = std::strchr(data, '='); @@ -35,7 +36,7 @@ std::vector& Environment() { #endif if (source) for (; *source; ++source) { if (**source && **source != '=' && std::strchr(*source, '=')) - result.push_back({*source, nullptr}); + result.push_back({std::make_unique(*source), nullptr}); } return result; }(); @@ -66,7 +67,7 @@ int APS5_VABI setenv_nid_postfix(const char* name, const char* value, int overwr auto found = std::find_if(entries.begin(), entries.end(), [name](auto& entry) { return entry.Matches(name); }); if (found != entries.end() && !overwrite) return 0; - EnvironmentEntry replacement{std::string(name) + '=' + value, nullptr}; + EnvironmentEntry replacement{std::make_unique(std::string(name) + '=' + value), nullptr}; if (found != entries.end()) *found = std::move(replacement); else entries.push_back(std::move(replacement)); return 0; @@ -93,7 +94,7 @@ int APS5_VABI putenv_nid_postfix(char* text) { const std::string_view name(text, separator - text); auto found = std::find_if(entries.begin(), entries.end(), [name](auto& entry) { return entry.Matches(name); }); - EnvironmentEntry replacement{{}, text}; + EnvironmentEntry replacement{nullptr, text}; if (found != entries.end()) *found = std::move(replacement); else entries.push_back(std::move(replacement)); return 0; diff --git a/core/libs/tests/GuestEnvironment.cpp b/core/libs/tests/GuestEnvironment.cpp index df1910cf4..42ae01cd4 100644 --- a/core/libs/tests/GuestEnvironment.cpp +++ b/core/libs/tests/GuestEnvironment.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include extern "C" { char* APS5_VABI getenv_nid_postfix(const char*); int APS5_VABI setenv_nid_postfix(const char*, const char*, int); @@ -43,4 +44,11 @@ int main() { Require(getenv_nid_postfix(key) == nullptr); Require(unsetenv_nid_postfix(key) == 0); Require(unsetenv_nid_postfix("anyps5_guest_env_test_4c27") == 0); + Require(setenv_nid_postfix("Q7A", "a", 1) == 0); + Require(setenv_nid_postfix("Q7B", "b", 1) == 0); + const char* kept = getenv_nid_postfix("Q7B"); + Require(unsetenv_nid_postfix("Q7A") == 0); + for (int index = 0; index < 64; ++index) + Require(setenv_nid_postfix(("ANYPS5_GUEST_ENV_GROW_4C27_" + std::to_string(index)).c_str(), "x", 1) == 0); + Require(getenv_nid_postfix("Q7B") == kept && std::strcmp(kept, "b") == 0); } From f30f3e8a84a7ba5d56e33acc6a237144dd9ca7a1 Mon Sep 17 00:00:00 2001 From: Ahir Barman Maji <191682168+ahir061@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:38:05 +0530 Subject: [PATCH 093/318] fix(libs): find the executable directory on macOS libSceFont and libSceNpEntitlementAccess located the executable through /proc/self/exe, which macOS does not have, so finding the system fonts and the entitlements file threw there. Use _NSGetExecutablePath and resolve the path with std::filesystem::canonical on macOS. Linux and Windows are unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/src/SystemFont.cpp | 11 +++++++++++ core/libs/prx/libSceNpEntitlementAccess/Export.cpp | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index bf1281380..f164d8646 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -6,6 +6,8 @@ #include #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include #endif #include "prx/libSceFont/include/FontInternal.hpp" @@ -91,6 +93,15 @@ std::filesystem::path ExecutableDirectory() { const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); if (length == 0 || length == MAX_PATH) throw std::runtime_error("libSceFont: cannot locate the executable"); return std::filesystem::path(module).parent_path(); +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::error_code error; + std::filesystem::path path; + if (_NSGetExecutablePath(executable.data(), &size) == 0) path = std::filesystem::canonical(executable.data(), error); + if (path.empty()) throw std::runtime_error("libSceFont: cannot locate the executable"); + return path.parent_path(); #else return std::filesystem::read_symlink("/proc/self/exe").parent_path(); #endif diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index d0bc1f710..1b5a443de 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -12,6 +12,8 @@ #include #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include #endif static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER = static_cast(0x80558003); @@ -28,6 +30,15 @@ std::filesystem::path EntitlementsPath() { const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); if (length == 0 || length == MAX_PATH) throw std::runtime_error("NpEntitlementAccess: cannot locate the executable"); return std::filesystem::path(module).parent_path() / "anyps5-entitlements.ini"; +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::error_code error; + std::filesystem::path path; + if (_NSGetExecutablePath(executable.data(), &size) == 0) path = std::filesystem::canonical(executable.data(), error); + if (path.empty()) throw std::runtime_error("NpEntitlementAccess: cannot locate the executable"); + return path.parent_path() / "anyps5-entitlements.ini"; #else return std::filesystem::read_symlink("/proc/self/exe").parent_path() / "anyps5-entitlements.ini"; #endif From 9d8ee3de7d99c77bee504069fbcf1e58132c79fd Mon Sep 17 00:00:00 2001 From: Mouhand Kaddo <73709175+Mouhand-Kaddo@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:12:17 +0400 Subject: [PATCH 094/318] fix(shader): isolate single-block loop continue targets Route semantic self-loops through a dedicated empty back-edge block. This prevents block merging from folding the continue target into the loop header when memory instructions emit nested selections. Add a synthetic loop-store regression that fails SPIR-V validation after optimization on main. --- core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp | 10 ++++++++++ .../shader/recompiler/ControlFlow/src/Structurizer.cpp | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index 030af961d..96708260f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -566,6 +566,16 @@ done: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0x7d880090u, 0xbf860004u, 0x7d880088u, 0xbf870003u, 0x4a020081u, 0xbf820005u, 0x4a020082u, 0x7d880084u, 0xbf860001u, 0x4a020285u, 0x4a020283u}), Split::Clone}, + {"memory access in a single-block loop latch", R"( + s_mov_b32 s8, 0 + v_mov_b32 v1, 0 +loop: + buffer_store_dword v1, off, s[0:3], 0 + s_add_u32 s8, s8, 1 + s_cmp_lt_u32 s8, 4 + s_cbranch_scc1 loop + s_endpgm)", + {0xbe880380u, 0x7e020280u, 0xe0700000u, 0x80000100u, 0x80088108u, 0xbf0a8408u, 0xbf85fffbu, 0xbf810000u}, Split::None}, {"shared tail in a loop", R"( s_mov_b32 s8, 0 v_mov_b32 v1, 0 diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index f3a5b7f6f..1b500f598 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1719,7 +1719,9 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeL const bool latchInInnerLoop = routeSharedExits && latches.size() == 1u && std::any_of(graph.naturalLoops.begin(), graph.naturalLoops.end(), [&](const NaturalLoop& inner) { return inner.headerBlock != loop.headerBlock && contains(loop.blocks, inner.headerBlock) && contains(inner.blocks, latches.front()); }); - if (latches.size() <= 1u && !latchInInnerLoop) { + const auto& header = graph.FindBlock(loop.headerBlock); + const bool semanticSelfLoop = latches.size() == 1u && latches.front() == header.id && header.instructionBegin != header.instructionEnd; + if (latches.size() <= 1u && !latchInInnerLoop && !semanticSelfLoop) { continue; } From 3ba4532352952ef4b3e804fb32f984fb36a19e41 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 11 Oct 2026 09:41:49 -0700 Subject: [PATCH 095/318] fix(libkernel): reset a once control when its initializer exits or is cancelled pthread_once and scePthreadOnce marked the control as running, called the initializer and marked the control done only after it returned. A thread that left the initializer through pthread_exit or a cancellation point never came back, so the control stayed in progress and every later call on it waited forever. FreeBSD libthr runs the initializer under a cleanup handler (lib/libthr/thread/thr_once.c, _pthread_once: "THR_CLEANUP_PUSH(curthread, once_cancel_handler, once_control); init_routine(); THR_CLEANUP_POP(curthread, 0);"). once_cancel_handler stores ONCE_NEVER_DONE and wakes the waiters, and thread exit and cancellation run the cleanup handlers. POSIX requires this for cancellation: "if init_routine is a cancellation point and is canceled, the effect on once_control shall be as if pthread_once() was never called." Both functions now push such a handler around the initializer, so the control returns to its initial state and the next caller, or one already waiting, runs the initializer. scePthreadOnce waiters now retry when the control goes back to its initial state, and an exception from its initializer resets the control as it already did for pthread_once. CleanupHandler moves to Cancel.hpp so that Once.cpp can use it. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libkernel/Pthread/Posix/Once.cpp | 17 +++++-- .../prx/libkernel/Pthread/include/Cancel.hpp | 7 +++ .../libs/prx/libkernel/Pthread/src/Thread.cpp | 34 ++++++++------ core/libs/tests/GuestOnce.cpp | 46 ++++++++++++++++++- 4 files changed, 85 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp index 32ed2ee79..dc09e2cb4 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include #include #include @@ -14,6 +15,14 @@ struct GuestOnce { static_assert(offsetof(GuestOnce, mutex) == 8 && sizeof(GuestOnce) == 16); std::mutex onceMutex; std::condition_variable onceChanged; + +void APS5_VABI ResetOnce(void* control) { + { + std::lock_guard lock(onceMutex); + static_cast(control)->state = 0; + } + onceChanged.notify_all(); +} } extern "C" int APS5_VABI pthread_once_nid_postfix( @@ -27,15 +36,15 @@ extern "C" int APS5_VABI pthread_once_nid_postfix( lock.unlock(); // Initializers may initialize other once controls. Never run guest code // under the shared lock. The lock publishes all initializer writes. + CleanupHandler reset{}; + __pthread_cleanup_push_imp_nid_postfix(ResetOnce, control, &reset); try { initialize(); } catch (...) { - lock.lock(); - control->state = 0; - lock.unlock(); - onceChanged.notify_all(); + __pthread_cleanup_pop_imp_nid_postfix(1); throw; } + __pthread_cleanup_pop_imp_nid_postfix(0); lock.lock(); control->state = 1; lock.unlock(); diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index 26a7c7176..b85a2a375 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -12,6 +12,13 @@ inline constexpr int GUEST_CANCEL_DEFERRED = 0; inline constexpr int GUEST_CANCEL_ASYNCHRONOUS = 2; inline void* const GUEST_CANCELED = reinterpret_cast(std::uintptr_t{1}); +struct CleanupHandler { + CleanupHandler* previous; + void (APS5_VABI* routine)(void*); + void* argument; + int onHeap; +}; + extern "C" { void APS5_VABI __pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info); void APS5_VABI __pthread_cleanup_pop_imp_nid_postfix(int execute); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 11400bdd2..cc471f5f5 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -53,12 +53,6 @@ struct ThreadArgs { static std::atomic threadDtors{nullptr}; static std::atomic threadAtexitCount{nullptr}; static std::atomic threadAtexitReport{nullptr}; -struct CleanupHandler { - CleanupHandler* previous; - void (APS5_VABI* routine)(void*); - void* argument; - int onHeap; -}; struct GuestThreadState { PthreadPrivate* current = nullptr; @@ -802,20 +796,34 @@ int APS5_VABI scePthreadSetprio(Pthread thread, int prio) { return SCE_OK; } +static void APS5_VABI ResetOnce(void* once) { + constexpr int32_t Never = 0; + std::atomic_ref state(*static_cast(once)); + state.store(Never, std::memory_order_release); + state.notify_all(); +} + int APS5_VABI scePthreadOnce(int32_t* once, void (APS5_VABI* init)(void)) { if (!once || !init) return SCE_KERNEL_ERROR_EINVAL; constexpr int32_t Never = 0; constexpr int32_t Done = 1; constexpr int32_t Running = 2; std::atomic_ref state(*once); - int32_t expected = Never; - if (state.compare_exchange_strong(expected, Running, std::memory_order_acq_rel)) { - init(); - state.store(Done, std::memory_order_release); - state.notify_all(); - return SCE_OK; + for (int32_t expected = Never; !state.compare_exchange_strong(expected, Running, std::memory_order_acq_rel); expected = Never) { + if (expected != Running) return SCE_OK; + state.wait(Running, std::memory_order_acquire); } - while ((expected = state.load(std::memory_order_acquire)) == Running) state.wait(Running, std::memory_order_acquire); + CleanupHandler reset{}; + __pthread_cleanup_push_imp_nid_postfix(ResetOnce, once, &reset); + try { + init(); + } catch (...) { + __pthread_cleanup_pop_imp_nid_postfix(1); + throw; + } + __pthread_cleanup_pop_imp_nid_postfix(0); + state.store(Done, std::memory_order_release); + state.notify_all(); return SCE_OK; } diff --git a/core/libs/tests/GuestOnce.cpp b/core/libs/tests/GuestOnce.cpp index b82ec22ef..ea35859ab 100644 --- a/core/libs/tests/GuestOnce.cpp +++ b/core/libs/tests/GuestOnce.cpp @@ -1,4 +1,4 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include #include #include @@ -9,7 +9,16 @@ struct GuestOnce { std::int32_t state; void* mutex; }; static_assert(sizeof(GuestOnce) == 16 && offsetof(GuestOnce, mutex) == 8); using Initializer = void (APS5_VABI *)(); -extern "C" int APS5_VABI pthread_once_nid_postfix(GuestOnce*, Initializer); +extern "C" { +int APS5_VABI pthread_once_nid_postfix(GuestOnce*, Initializer); +int APS5_VABI scePthreadOnce(std::int32_t* once, Initializer init); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +void APS5_VABI scePthreadExit(void* retval); +int APS5_VABI scePthreadCancel(Pthread thread); +void APS5_VABI scePthreadTestcancel(); +Pthread APS5_VABI scePthreadSelf(); +} static void Require(bool value) { if (!value) std::abort(); } static GuestOnce control{}, nested{}, retry{}; static std::atomic calls{0}, arrived{0}; @@ -24,6 +33,27 @@ static void APS5_VABI Initialize() { static void APS5_VABI Retry() { if (++attempts == 1) throw 7; } +static GuestOnce exited{}, cancelled{}; +static std::int32_t sceExited = 0, sceCancelled = 0; +static int retried = 0; +static void APS5_VABI ExitInsideInitializer() { scePthreadExit(nullptr); } +static void APS5_VABI CancelInsideInitializer() { + Require(scePthreadCancel(scePthreadSelf()) == 0); + scePthreadTestcancel(); +} +static void APS5_VABI CountRetry() { ++retried; } +struct OnceCall { GuestOnce* control; std::int32_t* sceControl; Initializer initialize; }; +static void* APS5_VABI CallOnce(void* argument) { + const auto& call = *static_cast(argument); + if (call.control) pthread_once_nid_postfix(call.control, call.initialize); + else scePthreadOnce(call.sceControl, call.initialize); + return nullptr; +} +static void LeaveInsideInitializer(OnceCall call) { + Pthread thread{}; + Require(scePthreadCreate(&thread, nullptr, CallOnce, &call, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); +} int main() { std::array workers; for (auto& worker : workers) worker = std::thread([] { @@ -41,4 +71,16 @@ int main() { Require(pthread_once_nid_postfix(&control, nullptr) == 22); GuestOnce invalid{9, nullptr}; Require(pthread_once_nid_postfix(&invalid, Initialize) == 22); + LeaveInsideInitializer({&exited, nullptr, ExitInsideInitializer}); + Require(exited.state == 0); + Require(pthread_once_nid_postfix(&exited, CountRetry) == 0 && exited.state == 1 && retried == 1); + LeaveInsideInitializer({&cancelled, nullptr, CancelInsideInitializer}); + Require(cancelled.state == 0); + Require(pthread_once_nid_postfix(&cancelled, CountRetry) == 0 && cancelled.state == 1 && retried == 2); + LeaveInsideInitializer({nullptr, &sceExited, ExitInsideInitializer}); + Require(sceExited == 0); + Require(scePthreadOnce(&sceExited, CountRetry) == 0 && sceExited == 1 && retried == 3); + LeaveInsideInitializer({nullptr, &sceCancelled, CancelInsideInitializer}); + Require(sceCancelled == 0); + Require(scePthreadOnce(&sceCancelled, CountRetry) == 0 && sceCancelled == 1 && retried == 4); } From e8cb97e6e2c47d8439358dcfca658bbb4d32d596 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 11 Oct 2026 09:44:11 -0700 Subject: [PATCH 096/318] fix(libkernel): fail rmdir on a symbolic link instead of removing it sceKernelRmdir, and rmdir through it, checked the path with std::filesystem::is_directory and is_empty, which follow a symbolic link, and then removed the path itself. So rmdir on a link to an empty directory returned 0 and deleted the link, while the target stayed. FreeBSD looks up the last component of rmdir's path without following it (sys/kern/vfs_syscalls.c, kern_rmdirat: NDINIT_ATRIGHTS with LOCKPARENT | LOCKLEAF and no FOLLOW) and then fails with ENOTDIR for anything that is not a directory: "if (vp->v_type != VDIR) { error = ENOTDIR; goto out; }". A link, whether it points to a directory or nowhere, is a VLNK there. rmdir now returns ENOTDIR for a symbolic link and leaves it alone, as unlink already looks at the link itself through symlink_status. A trailing slash still resolves through the link, as before. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 1 + core/libs/tests/GuestFilesystem.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index d383ac3c8..d17028cac 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1028,6 +1028,7 @@ int APS5_VABI sceKernelRmdir(const char* path) { std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; + if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GUEST_ENOTDIR); if (!std::filesystem::is_directory(native, error)) { const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); return SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : GUEST_ENOENT); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 5474dee06..885b99ce8 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -580,6 +580,20 @@ int main() { Require(rmdir_nid_postfix((missingName + "/").c_str()) == -1 && *__error_nid_postfix() == 2); Require(sceKernelRmdir((presentName + "/").c_str()) == static_cast(0x80020014u)); Require(sceKernelRmdir((missingName + "/").c_str()) == static_cast(0x80020002u)); +#ifndef _WIN32 + const auto linkedDirectory = root / "linked-directory"; + const auto directoryLink = root / "directory-link"; + const auto danglingDirectoryLink = root / "dangling-directory-link"; + Require(std::filesystem::create_directory(linkedDirectory)); + std::filesystem::create_directory_symlink("linked-directory", directoryLink); + std::filesystem::create_directory_symlink("missing-directory", danglingDirectoryLink); + Require(rmdir_nid_postfix(directoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelRmdir(directoryLink.string().c_str()) == static_cast(0x80020014u)); + Require(rmdir_nid_postfix(danglingDirectoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(std::filesystem::is_symlink(directoryLink) && std::filesystem::is_directory(linkedDirectory)); + Require(unlink_nid_postfix(directoryLink.string().c_str()) == 0 && unlink_nid_postfix(danglingDirectoryLink.string().c_str()) == 0); + Require(rmdir_nid_postfix(linkedDirectory.string().c_str()) == 0 && !std::filesystem::exists(linkedDirectory)); +#endif const auto dotDirectory = root / "dot-directory"; Require(std::filesystem::create_directory(dotDirectory)); const auto dotDirectoryName = dotDirectory.string() + "/."; From 942148457206f0491a461cad6db85d8de7ee1549 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 11 Oct 2026 10:23:58 -0700 Subject: [PATCH 097/318] fix(libkernel): fail open with O_NOFOLLOW on a symbolic link sceKernelOpen built the host flags from the bits it knows and dropped O_NOFOLLOW (0x100), so open and sceKernelOpen with O_NOFOLLOW followed a symbolic link and opened its target, and with O_CREAT created the missing target of a dangling link. FreeBSD looks up the last component without following it when O_NOFOLLOW is set (sys/kern/vfs_vnops.c, vn_open_cred: "((fmode & O_NOFOLLOW) ? NOFOLLOW : FOLLOW) | LOCKLEAF", and with O_CREAT "if ((fmode & O_EXCL) == 0 && (fmode & O_NOFOLLOW) == 0) ndp->ni_cnd.cn_flags |= FOLLOW;"), and then refuses the link itself in vn_open_vnode: "if (vp->v_type == VLNK) return (EMLINK);". open now fails with EMLINK when O_NOFOLLOW is set and the path names a symbolic link. O_CREAT | O_EXCL keeps failing with EEXIST, and a trailing slash still resolves through the link. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/include/FileFlags.hpp | 1 + core/libs/prx/libkernel/File/src/Open.cpp | 5 +++++ core/libs/tests/GuestFilesystem.cpp | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/core/libs/prx/libkernel/File/include/FileFlags.hpp b/core/libs/prx/libkernel/File/include/FileFlags.hpp index f97ccc9d2..2423ccdac 100644 --- a/core/libs/prx/libkernel/File/include/FileFlags.hpp +++ b/core/libs/prx/libkernel/File/include/FileFlags.hpp @@ -9,6 +9,7 @@ constexpr std::int32_t SCE_KERNEL_O_RDWR = 0x00000002; constexpr std::int32_t SCE_KERNEL_O_ACCMODE = 0x00000003; constexpr std::int32_t SCE_KERNEL_O_APPEND = 0x00000008; constexpr std::int32_t SCE_KERNEL_O_FSYNC = 0x00000080; +constexpr std::int32_t SCE_KERNEL_O_NOFOLLOW = 0x00000100; constexpr std::int32_t SCE_KERNEL_O_SYNC = 0x00000080; constexpr std::int32_t SCE_KERNEL_O_CREAT = 0x00000200; constexpr std::int32_t SCE_KERNEL_O_TRUNC = 0x00000400; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 66250bf90..2166b03a0 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -152,6 +152,11 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { if (std::filesystem::exists(status) && !std::filesystem::is_directory(status)) return SceErrorFromErrno(ENOTDIR); } #endif + if ((flags & SCE_KERNEL_O_NOFOLLOW) != 0 && (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) != (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) { + constexpr int GuestEmlink = 31; + std::error_code error; + if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GuestEmlink); + } int fd = NativeOpen(native, MapFlags(flags), mode); #ifdef _WIN32 if (fd < 0 && errno != ENOENT) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 5474dee06..c9e8cc67e 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -409,6 +409,16 @@ int main() { const auto dangling = root / "dangling"; std::filesystem::create_symlink("missing.txt", dangling); Require(lstat_nid_postfix(dangling.string().c_str(), &linkStatus) == 0 && (linkStatus.st_mode & 0170000) == 0120000); + constexpr int noFollow = 0x100; + Require(sceKernelOpen(link.string().c_str(), noFollow, 0) == static_cast(0x8002001Fu)); + Require(open_nid_postfix(link.string().c_str(), noFollow, 0) == -1 && *__error_nid_postfix() == 31); + Require(open_nid_postfix(dangling.string().c_str(), noFollow | 0x201, 0600) == -1 && *__error_nid_postfix() == 31); + Require(!std::filesystem::exists(root / "missing.txt")); + Require(open_nid_postfix(dangling.string().c_str(), noFollow | 0xA01, 0600) == -1 && *__error_nid_postfix() == 17); + const int followed = open_nid_postfix(link.string().c_str(), 0, 0); + Require(followed >= 0 && close_nid_postfix(followed) == 0); + const int plain = open_nid_postfix(presentName.c_str(), noFollow, 0); + Require(plain >= 0 && close_nid_postfix(plain) == 0); Require(stat_nid_postfix(dangling.string().c_str(), &status) == -1 && *__error_nid_postfix() == 2); Require(lstat_nid_postfix((root / "present.txt" / "child").string().c_str(), &linkStatus) == -1 && *__error_nid_postfix() == 20); const auto viaMissing = rootName + "/missing/../present.txt"; From 121b16fb4bbd47ff4ce9302c40631bab858e3e71 Mon Sep 17 00:00:00 2001 From: Mouhand Kaddo <73709175+Mouhand-Kaddo@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:33:53 +0400 Subject: [PATCH 098/318] fix(libkernel): guard Unix host polling in Windows select --- core/libs/prx/libkernel/Socket/src/Socket.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 3f71f4f1c..89c4f9c26 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -186,6 +186,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* waitMilliseconds = static_cast(std::clamp(roundedMilliseconds, 0, 100)); } if (devicesReady) waitMilliseconds = 0; +#ifndef _WIN32 if (!hosts.empty() && !sockets.empty()) { if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); const auto sliceEnd = std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds); @@ -214,7 +215,9 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* return -1; } for (std::size_t index = 0; index < hosts.size(); ++index) requested[hostSlots[index]].revents = hosts[index].revents; - } else if (!sockets.empty()) { + } else +#endif + if (!sockets.empty()) { if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); for (auto& socket : sockets) socket.revents = 0; const int result = poller(sockets.data(), static_cast(sockets.size()), waitMilliseconds); From 58d59f6279fd51afde820b19b79ac82260cf042f Mon Sep 17 00:00:00 2001 From: ThatsLeo Date: Sun, 11 Oct 2026 21:57:54 +0200 Subject: [PATCH 099/318] fix(shader): fold a lane's s_wqm bit of the starting pixel lanes to true Vulkan starts only quads holding a non-helper lane, so the bit is set in every lane it runs. Stray's pixel shader 0x2012f90000 keeps the packed ancillary word where that bit is clear, and failed with an unsupported live use. (cherry picked from commit 2068089b3d75fae55d0994a0916a22bffd7ee2a4) --- core/libs/tests/PixelAncillary.cpp | 72 +++++++++++++++++ .../Optimization/src/ConstantFolder.cpp | 80 ++++++++++++++++++- 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index 15740c514..69ce4d7c2 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -427,6 +427,74 @@ static void HelperLoopRefused(LoopRead read) { } Require(false); } +enum class StartingQuad { Kept, BallotAfterBranch, BallotOfOtherLanes, ComputeStage }; + +static IrValue& StartingQuadMasked(IrProgram& program, StartingQuad shape) { + auto& entry = Begin(program); + if (shape == StartingQuad::ComputeStage) program.Resources().stage = IrShaderStage::Compute; + IrBlock* tail = &entry; + if (shape == StartingQuad::BallotAfterBranch) { + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + tail = &merge; + } + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& started = shape == StartingQuad::BallotOfOtherLanes ? FrontFacingBuiltin(builder) : builder.IEqual(helper, builder.Constant(0u)); + IrValue& live = builder.Emit(IrOpcode::Ballot, IrType::Vec4U32, {&started}); + builder.SetInsertionPoint(*tail); + IrValue& active = builder.Emit(IrOpcode::Ballot, IrType::Vec4U32, {&builder.ConstantBool(true)}); + IrValue& low = builder.BitwiseAnd(builder.CompositeExtract(live, 0u), builder.CompositeExtract(active, 0u)); + IrValue& high = builder.BitwiseAnd(builder.CompositeExtract(live, 1u), builder.CompositeExtract(active, 1u)); + IrValue& quads = builder.Emit(IrOpcode::WqmU64, IrType::U64, {&builder.Emit(IrOpcode::CompositeConstructU64, IrType::U64, {&low, &high})}); + IrValue& lane = LaneBit(builder, builder.Emit(IrOpcode::CompositeExtractU64, IrType::U32, {&quads, &builder.Constant(0u)}), builder.Emit(IrOpcode::CompositeExtractU64, IrType::U32, {&quads, &builder.Constant(1u)})); + IrValue& kept = builder.Select(lane, builder.Constant(SelectOther), ancillary); + End(builder, kept); + return kept; +} +static const IrValue* StartingQuadExported(IrProgram& program, StartingQuad shape) { + static_cast(StartingQuadMasked(program, shape)); + if (shape == StartingQuad::ComputeStage) { + ConstantFolder().Fold(program); + DeadCodeEliminator().Eliminate(program); + } else { + Lower(program); + } + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::ReferenceU32) return inst->Argument(0)->Resolve(); + } + } + return nullptr; +} +static void StartingQuadOnly() { + IrProgram program; + const IrValue* value = StartingQuadExported(program, StartingQuad::Kept); + Require(value != nullptr && value->HasImmediate() && value->ImmediateU32() == SelectOther); +} +static void StartingQuadOutsidePixel() { + IrProgram program; + const IrValue* value = StartingQuadExported(program, StartingQuad::ComputeStage); + Require(value != nullptr && value->Opcode() == IrOpcode::SelectU32); +} +static void StartingQuadRefused(StartingQuad shape) { + IrProgram program; + static_cast(StartingQuadMasked(program, shape)); + try { + Lower(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} int main() { Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); @@ -476,4 +544,8 @@ int main() { HelperLoopOnly(LoopRead::ExportInside); HelperLoopOnly(LoopRead::UniformLoop); HelperLoopRefused(LoopRead::RevivedExportInside); + StartingQuadOnly(); + StartingQuadRefused(StartingQuad::BallotAfterBranch); + StartingQuadRefused(StartingQuad::BallotOfOtherLanes); + StartingQuadOutsidePixel(); } diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 0020b87cb..21f3b0c34 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -1,5 +1,6 @@ #include "Optimization/ConstantFolder.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" +#include "IntermediateRepresentation/IrMetadata/StageIO.hpp" #include #include @@ -76,6 +77,83 @@ bool foldConstantBitTest(IrBuilder& builder, IrValue& inst) { return true; } +bool isLaneIndex(const IrValue& value) { + return value.Opcode() == IrOpcode::LaneId; +} + +bool isImmediateU32(const IrValue& value, std::uint32_t immediate) { + return isImmediate(value, IrType::U32) && value.ImmediateU32() == immediate; +} + +bool runsAllStartingLanes(const IrProgram& program, const IrBlock* target) { + if (program.BlockOrder().empty()) return false; + const IrBlock* block = program.BlockOrder().front(); + for (std::size_t steps = 0; block != nullptr && steps < program.BlockOrder().size(); ++steps) { + if (block == target) return true; + if (block->Successors().size() != 1u) return false; + block = block->Successors().front(); + if (block->Predecessors().size() != 1u) return false; + } + return false; +} + +bool holdsStartingLanes(const IrProgram& program, const IrValue& word, std::uint32_t index, std::uint32_t depth = 0) { + if (depth > 8u) return false; + if (word.Opcode() == IrOpcode::BitwiseAnd32) { + return holdsStartingLanes(program, resolveArg(word, 0), index, depth + 1u) && holdsStartingLanes(program, resolveArg(word, 1), index, depth + 1u); + } + if (word.Opcode() == IrOpcode::BitwiseOr32) { + return holdsStartingLanes(program, resolveArg(word, 0), index, depth + 1u) || holdsStartingLanes(program, resolveArg(word, 1), index, depth + 1u); + } + if (word.Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediateU32(resolveArg(word, 1), index)) return false; + const auto& ballot = resolveArg(word, 0); + if (ballot.Opcode() != IrOpcode::Ballot || !runsAllStartingLanes(program, ballot.Parent())) return false; + const auto& condition = resolveArg(ballot, 0); + if (isImmediate(condition, IrType::U1)) return condition.ImmediateBool(); + if (condition.Opcode() != IrOpcode::IEqual32) return false; + const auto helper = [](const IrValue& value) { + if (value.Opcode() != IrOpcode::GetBuiltin) return false; + const auto& kind = resolveArg(value, 0); + return isImmediate(kind, IrType::U32) && static_cast(kind.ImmediateU32()) == StageInputKind::HelperInvocation; + }; + const auto& lhs = resolveArg(condition, 0); + const auto& rhs = resolveArg(condition, 1); + return (helper(lhs) && isImmediateU32(rhs, 0u)) || (helper(rhs) && isImmediateU32(lhs, 0u)); +} + +bool foldStartingQuadBitTest(const IrProgram& program, IrBuilder& builder, IrValue& inst) { + if (program.Resources().stage != IrShaderStage::Pixel) return false; + auto& left = resolveArg(inst, 0); + auto& right = resolveArg(inst, 1); + if (!isImmediateU32(right, 0u) || left.Opcode() != IrOpcode::BitwiseAnd32) return false; + IrValue* shifted = isImmediateU32(resolveArg(left, 1), 1u) ? &resolveArg(left, 0) : isImmediateU32(resolveArg(left, 0), 1u) ? &resolveArg(left, 1) : nullptr; + if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return false; + const auto& amount = resolveArg(*shifted, 1); + if (amount.Opcode() != IrOpcode::BitwiseAnd32) return false; + const bool laneAmount = (isLaneIndex(resolveArg(amount, 0)) && isImmediateU32(resolveArg(amount, 1), 31u)) || (isLaneIndex(resolveArg(amount, 1)) && isImmediateU32(resolveArg(amount, 0), 31u)); + if (!laneAmount) return false; + const auto& word = resolveArg(*shifted, 0); + const IrValue* low = &word; + const IrValue* high = nullptr; + if (word.Opcode() == IrOpcode::SelectU32) { + const auto& half = resolveArg(word, 0); + if (half.Opcode() != IrOpcode::ULessThan32 || !isLaneIndex(resolveArg(half, 0)) || !isImmediateU32(resolveArg(half, 1), 32u)) return false; + low = &resolveArg(word, 1); + high = &resolveArg(word, 2); + } else if (program.WaveSize() != 32u) { + return false; + } + if (low->Opcode() != IrOpcode::CompositeExtractU64 || !isImmediateU32(resolveArg(*low, 1), 0u)) return false; + const auto& wqm = resolveArg(*low, 0); + if (high != nullptr && (high->Opcode() != IrOpcode::CompositeExtractU64 || &resolveArg(*high, 0) != &wqm || !isImmediateU32(resolveArg(*high, 1), 1u))) return false; + if (wqm.Opcode() != IrOpcode::WqmU64) return false; + const auto& lanes = resolveArg(wqm, 0); + if (lanes.Opcode() != IrOpcode::CompositeConstructU64 || !holdsStartingLanes(program, resolveArg(lanes, 0), 0u)) return false; + if (high != nullptr && !holdsStartingLanes(program, resolveArg(lanes, 1), 1u)) return false; + replaceWith(inst, builder.ConstantBool(true)); + return true; +} + template bool foldU32Compare(IrBuilder& builder, IrValue& inst, TFunction function) { auto& lhs = resolveArg(inst, 0); auto& rhs = resolveArg(inst, 1); @@ -652,7 +730,7 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { return foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return std::max(a, b); }); } case IrOpcode::IEqual32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a == b; }); - case IrOpcode::INotEqual32: return foldConstantBitTest(builder, value) || foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a != b; }); + case IrOpcode::INotEqual32: return foldConstantBitTest(builder, value) || foldStartingQuadBitTest(program, builder, value) || foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a != b; }); case IrOpcode::ULessThan32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a < b; }); case IrOpcode::ULessThanEqual32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a <= b; }); case IrOpcode::UGreaterThan32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a > b; }); From 58ee73f4751b0962a562c9e7900f469e70c11583 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:42:53 +0200 Subject: [PATCH 100/318] fix(libc): write the whole what() when terminate reports an exception The default terminate handler formatted its whole report into a 1024-byte buffer, so a long what() was cut off without a trailing newline. Driver refusals that carry a serialized RecompileRequest lost most of it, and the next log line ran into the truncated text. The report header still goes through the bounded RawLog, and what() is now written in full by a raw write to the stderr handle, followed by a newline. Both keep avoiding the CRT stream lock, as before. exception_runtime gains a forked child that throws an uncaught runtime_error with a message over 3000 characters and checks that the full what() reaches stderr before SIGABRT. --- core/libs/prx/libc/src/exception/Runtime.cpp | 32 ++++++++++++++------ core/libs/tests/ExceptionRuntime.cpp | 24 +++++++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index 7b18999b7..9725d7125 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -1,6 +1,8 @@ #include "prx/libc/include/exceptions/Runtime.hpp" +#include #include #include +#include #include #ifdef _WIN32 #include @@ -10,6 +12,21 @@ namespace LibcException { // Writes straight to the stderr handle: terminate paths must not wait on the CRT stream lock, // which another (possibly suspended) thread may hold. +static void RawWrite(const char* data, std::size_t size) { +#ifdef _WIN32 + const HANDLE handle = GetStdHandle(STD_ERROR_HANDLE); + while (size != 0) { + DWORD written = 0; + const auto chunk = static_cast(std::min(size, std::numeric_limits::max())); + if (!WriteFile(handle, data, chunk, &written, nullptr) || written == 0) return; + data += written; + size -= written; + } +#else + std::fwrite(data, 1, size, stderr); +#endif +} + static void RawLog(const char* format, ...) { char buffer[1024]; va_list args; @@ -18,12 +35,7 @@ static void RawLog(const char* format, ...) { va_end(args); if (length <= 0) return; if (length > static_cast(sizeof(buffer)) - 1) length = sizeof(buffer) - 1; -#ifdef _WIN32 - DWORD written = 0; - WriteFile(GetStdHandle(STD_ERROR_HANDLE), buffer, static_cast(length), &written, nullptr); -#else - std::fwrite(buffer, 1, length, stderr); -#endif + RawWrite(buffer, static_cast(length)); } [[noreturn]] static void DefaultTerminate() { @@ -47,9 +59,11 @@ static void RawLog(const char* format, ...) { int status = 0; char* demangled = abi::__cxa_demangle(typeName, nullptr, nullptr, &status); const char* displayName = (status == 0 && demangled) ? demangled : typeName; - if (what) - RawLog("terminate called after throwing an instance of '%s'\n what(): %s\n", displayName, what); - else + if (what) { + RawLog("terminate called after throwing an instance of '%s'\n what(): ", displayName); + RawWrite(what, std::strlen(what)); + RawWrite("\n", 1); + } else RawLog("terminate called after throwing an instance of '%s'\n", displayName); std::free(demangled); } else { diff --git a/core/libs/tests/ExceptionRuntime.cpp b/core/libs/tests/ExceptionRuntime.cpp index c55a2cb15..a2dd29806 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -160,6 +162,27 @@ void CheckTerminate(void (*function)()) { assert(waitpid(child, &status, 0) == child); assert(WIFEXITED(status) && WEXITSTATUS(status) == 61); } +void CheckDefaultTerminateMessage() { + const std::string message = std::string(3000, 'w') + " end of message"; + int channel[2]; + assert(pipe(channel) == 0); + pid_t child = fork(); + assert(child >= 0); + if (child == 0) { + close(channel[0]); + dup2(channel[1], 2); + throw std::runtime_error(message); + } + close(channel[1]); + std::string output; + char buffer[4096]; + for (ssize_t count; (count = read(channel[0], buffer, sizeof(buffer))) > 0;) output.append(buffer, static_cast(count)); + close(channel[0]); + int status; + assert(waitpid(child, &status, 0) == child); + assert(WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT); + assert(output.find(" what(): " + message + "\n") != std::string::npos); +} struct ForcedGuard { ~ForcedGuard() { ++destroyed; } }; [[gnu::noinline]] void ForceUnwind() { ForcedGuard guard; @@ -231,6 +254,7 @@ int main() { CheckThreads(); CheckTerminate(UncaughtThrow); CheckTerminate(NoexceptThrow); + CheckDefaultTerminateMessage(); CheckForcedUnwind(); int frames = 0; auto backtrace = _Unwind_Backtrace_nid_postfix([](_Unwind_Context* context, void* argument) { From 91bfd6a18c1642c7ef8255b4cc487310e77e69da Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:59:12 +0530 Subject: [PATCH 101/318] feat(libkernel): implement pthread_attr_getinheritsched, pthread_attr_getscope and pthread_attr_setscope --- .../libs/prx/libkernel/Pthread/Posix/Attr.cpp | 23 ++++ .../prx/libkernel/Pthread/Posix/Common.hpp | 1 + .../prx/libkernel/Pthread/include/Pthread.hpp | 6 + core/libs/prx/libkernel/Pthread/src/Attr.cpp | 1 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 6 + core/libs/tests/GuestPthreadAttr.cpp | 107 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 7 files changed, 145 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index 0a889b3fd..89488dc78 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "../include/Pthread.hpp" @@ -33,6 +35,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) p->_schedpriority = thread->priority.load(std::memory_order_relaxed); p->_affinity = thread->affinity.load(std::memory_order_relaxed); p->_affinityExplicit = true; + p->_scope = thread->scope; return 0; } @@ -48,6 +51,13 @@ int APS5_VABI pthread_attr_getguardsize_nid_postfix(const PthreadAttr* attr, siz return 0; } +int APS5_VABI pthread_attr_getinheritsched_nid_postfix(const PthreadAttr* attr, int* inherit_sched) { + if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (!inherit_sched) throw std::runtime_error(std::string(__func__) + ": null output"); + *inherit_sched = (*attr)->_inheritsched; + return 0; +} + int APS5_VABI pthread_attr_getschedparam_nid_postfix(const PthreadAttr* attr, KernelSchedParam* param) { if (!Valid(attr) || !param) return PosixThread::GUEST_EINVAL; param->sched_priority = (*attr)->_schedpriority; @@ -60,6 +70,12 @@ int APS5_VABI pthread_attr_getschedpolicy_nid_postfix(const PthreadAttr* attr, i return 0; } +int APS5_VABI pthread_attr_getscope_nid_postfix(const PthreadAttr* attr, int* scope) { + if (!Valid(attr) || !scope) return PosixThread::GUEST_EINVAL; + *scope = (*attr)->_scope; + return 0; +} + int APS5_VABI pthread_attr_getstack_nid_postfix(const PthreadAttr* __restrict attr, void** __restrict stack_addr, size_t* __restrict stack_size) { if (!Valid(attr) || !stack_addr || !stack_size) return PosixThread::GUEST_EINVAL; *stack_addr = (*attr)->stackAddress; @@ -92,6 +108,7 @@ int APS5_VABI pthread_attr_setguardsize_nid_postfix(PthreadAttr* attr, size_t gu int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int inherit_sched) { if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (inherit_sched != GUEST_INHERIT_SCHED && inherit_sched != GUEST_EXPLICIT_SCHED) return PosixThread::GUEST_ENOTSUP; (*attr)->_inheritsched = inherit_sched; return 0; } @@ -108,6 +125,12 @@ int APS5_VABI pthread_attr_setschedpolicy_nid_postfix(PthreadAttr* attr, int pol return 0; } +int APS5_VABI pthread_attr_setscope_nid_postfix(PthreadAttr* attr, int scope) { + if (!Valid(attr) || (scope != GUEST_SCOPE_PROCESS && scope != GUEST_SCOPE_SYSTEM)) return PosixThread::GUEST_EINVAL; + (*attr)->_scope = scope; + return 0; +} + int APS5_VABI pthread_attr_setstacksize_nid_postfix(PthreadAttr* attr, size_t stack_size) { if (!Valid(attr)) return PosixThread::GUEST_EINVAL; return PosixThread::ToErrno(scePthreadAttrSetstacksize(attr, stack_size)); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Common.hpp b/core/libs/prx/libkernel/Pthread/Posix/Common.hpp index cf95f707e..dca57d9df 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Common.hpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Common.hpp @@ -13,6 +13,7 @@ namespace PosixThread { constexpr int GUEST_ESRCH = 3; constexpr int GUEST_EFAULT = 14; constexpr int GUEST_EINVAL = 22; +constexpr int GUEST_ENOTSUP = 45; constexpr int GUEST_ETIMEDOUT = 60; inline int ToErrno(int sceResult) { diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index f2df5c9b2..dfb04b5b3 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -83,6 +83,10 @@ static constexpr int DEFAULT_THREAD_PRIORITY = 700; static constexpr std::uint32_t CPU_CLOCK_BIT = 0x80000000u; static constexpr std::uint32_t CPU_CLOCK_PROCESS_BIT = 0x40000000u; static constexpr std::uint32_t CPU_CLOCK_ID_MASK = ~(CPU_CLOCK_BIT | CPU_CLOCK_PROCESS_BIT); +static constexpr int GUEST_EXPLICIT_SCHED = 0; +static constexpr int GUEST_INHERIT_SCHED = 4; +static constexpr int GUEST_SCOPE_PROCESS = 0; +static constexpr int GUEST_SCOPE_SYSTEM = 2; inline int GuestCpuFromHost(unsigned hostCpu, KernelCpumask threadAffinity) { KernelCpumask mask = threadAffinity & DEFAULT_THREAD_AFFINITY; @@ -102,6 +106,7 @@ struct PthreadAttrPrivate { bool _affinityExplicit = false; std::size_t _guardsize = 0x1000; int _solosched = 0; + int _scope = GUEST_SCOPE_SYSTEM; }; inline std::atomic nextThreadId{100000}; @@ -123,6 +128,7 @@ struct PthreadPrivate { std::atomic waitCount{0}; std::atomic affinity{DEFAULT_THREAD_AFFINITY}; std::atomic priority{DEFAULT_THREAD_PRIORITY}; + int scope = GUEST_SCOPE_SYSTEM; int cpuClockThread = 0; #ifndef _WIN32 clockid_t hostCpuClock{}; diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index ff5cba730..7b5b70e1d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -84,6 +84,7 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { (*attr)->_inheritsched = 4; (*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed); (*attr)->_affinityExplicit = true; + (*attr)->_scope = thread->scope; return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 11400bdd2..b02675d7a 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -529,6 +529,12 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread p->stackSize = attr ? (*attr)->_stacksize : DEFAULT_STACK_SIZE; if (attr) { p->priority.store((*attr)->_schedpriority, std::memory_order_relaxed); + if ((*attr)->_inheritsched != GUEST_INHERIT_SCHED) + p->scope = (*attr)->_scope; + else if (CurrentGuestState().current) + p->scope = CurrentGuestState().current->scope; + } else if (CurrentGuestState().current) { + p->scope = CurrentGuestState().current->scope; } if (name) p->name = name; std::promise start; diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index ee24e0000..97c22b9a0 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -22,11 +22,19 @@ int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state); int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched); int APS5_VABI pthread_attr_setstacksize_nid_postfix(PthreadAttr* attr, std::size_t stackSize); int APS5_VABI pthread_attr_setsolosched_np_nid_postfix(PthreadAttr* attr, int solosched); +int APS5_VABI pthread_attr_getinheritsched_nid_postfix(const PthreadAttr* attr, int* inherit_sched); +int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int inherit_sched); +int APS5_VABI pthread_attr_getscope_nid_postfix(const PthreadAttr* attr, int* scope); +int APS5_VABI pthread_attr_setscope_nid_postfix(PthreadAttr* attr, int scope); +int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr); } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int EXPLICIT_SCHED = 0; +static constexpr int INHERIT_SCHED = 4; +static constexpr int SCOPE_PROCESS = 0; +static constexpr int SCOPE_SYSTEM = 2; static constexpr int DETACH_JOINABLE = 0; static constexpr int CREATION_PRIORITY = 256; static constexpr int UPDATED_PRIORITY = 767; @@ -35,6 +43,7 @@ static constexpr KernelCpumask UPDATED_AFFINITY = 0x1000; static constexpr std::size_t STACK_SIZE = 2u << 20; static constexpr std::size_t MIN_STACK_SIZE = 16384; static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_ENOTSUP = 45; static void Require(bool value) { if (!value) std::abort(); } @@ -98,9 +107,107 @@ static void* APS5_VABI Worker(void* arg) { return nullptr; } +static void CheckSchedulingAttributes() { + int inherit = -1; + int scope = -1; + Require(pthread_attr_getinheritsched_nid_postfix(nullptr, &inherit) == GUEST_EINVAL); + Require(pthread_attr_setinheritsched_nid_postfix(nullptr, EXPLICIT_SCHED) == GUEST_EINVAL); + Require(pthread_attr_getscope_nid_postfix(nullptr, &scope) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(nullptr, SCOPE_SYSTEM) == GUEST_EINVAL); + + PthreadAttr attr = nullptr; + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == GUEST_EINVAL); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == GUEST_EINVAL); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == GUEST_EINVAL); + + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == INHERIT_SCHED); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, 1) == GUEST_ENOTSUP); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, -1) == GUEST_ENOTSUP); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == 0); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == EXPLICIT_SCHED); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, INHERIT_SCHED) == 0); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == INHERIT_SCHED); + + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(pthread_attr_setscope_nid_postfix(&attr, 1) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, 3) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, -1) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_PROCESS) == 0); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_PROCESS); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == 0); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + + Require(pthread_attr_getscope_nid_postfix(&attr, nullptr) == GUEST_EINVAL); + + bool threw = false; + try { + pthread_attr_getinheritsched_nid_postfix(&attr, nullptr); + } catch (const std::exception&) { + threw = true; + } + Require(threw); + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == GUEST_EINVAL); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == GUEST_EINVAL); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == GUEST_EINVAL); +} + +static void CheckThreadScope() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == 0); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_PROCESS) == 0); + + std::promise release; + auto released = release.get_future(); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, &attr, Worker, &released, nullptr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + + PthreadAttr queryAttr = nullptr; + Require(scePthreadAttrInit(&queryAttr) == SCE_OK); + Require(scePthreadAttrGet(thread, &queryAttr) == SCE_OK); + int scope = -1; + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_PROCESS); + + Require(pthread_attr_get_np_nid_postfix(thread, &queryAttr) == 0); + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_PROCESS); + Require(scePthreadAttrDestroy(&queryAttr) == SCE_OK); + + release.set_value(); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, INHERIT_SCHED) == 0); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_PROCESS) == 0); + + std::promise releaseInherited; + auto releasedInherited = releaseInherited.get_future(); + Pthread inheritedThread = nullptr; + Require(scePthreadCreate(&inheritedThread, &attr, Worker, &releasedInherited, nullptr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + + Require(scePthreadAttrInit(&queryAttr) == SCE_OK); + Require(scePthreadAttrGet(inheritedThread, &queryAttr) == SCE_OK); + scope = -1; + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(pthread_attr_get_np_nid_postfix(inheritedThread, &queryAttr) == 0); + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(scePthreadAttrDestroy(&queryAttr) == SCE_OK); + + releaseInherited.set_value(); + Require(scePthreadJoin(inheritedThread, nullptr) == SCE_OK); +} + int main() { CheckStackSizeLimit(); CheckSoloSched(); + CheckSchedulingAttributes(); + CheckThreadScope(); PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_OK); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fbf9f8e45..90e0dc5a6 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -386,6 +386,7 @@ Throughout the project, every function at every stage either **does exactly what - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [Draw input snapshots](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. +- [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: From 324669722e321896738b1a35a5e49f819bd71839 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:31:49 +0530 Subject: [PATCH 102/318] feat(libkernel): implement pthread_attr_getstackaddr --- core/libs/prx/libkernel/Pthread/Posix/Attr.cpp | 6 ++++++ core/libs/tests/GuestPthreadAttr.cpp | 12 ++++++++++-- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index 89488dc78..ea6ce0c0b 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -83,6 +83,12 @@ int APS5_VABI pthread_attr_getstack_nid_postfix(const PthreadAttr* __restrict at return 0; } +int APS5_VABI pthread_attr_getstackaddr_nid_postfix(const PthreadAttr* __restrict attr, void** __restrict stack_addr) { + if (!Valid(attr) || !stack_addr) return PosixThread::GUEST_EINVAL; + *stack_addr = (*attr)->stackAddress; + return 0; +} + int APS5_VABI pthread_attr_getstacksize_nid_postfix(const PthreadAttr* attr, size_t* stack_size) { if (!Valid(attr) || !stack_size) return PosixThread::GUEST_EINVAL; *stack_size = (*attr)->_stacksize; diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index 97c22b9a0..b6e41d395 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -57,6 +57,7 @@ struct ReportedAttributes { extern "C" { int APS5_VABI pthread_attr_setstack_nid_postfix(PthreadAttr* attr, void* addr, std::size_t size); int APS5_VABI pthread_attr_getstack_nid_postfix(const PthreadAttr* attr, void** addr, std::size_t* size); +int APS5_VABI pthread_attr_getstackaddr_nid_postfix(const PthreadAttr* attr, void** addr); } static ReportedAttributes Query(Pthread thread) { @@ -248,16 +249,23 @@ int main() { PthreadAttr stackAttr = nullptr; char stackMarker; + void* reportedAddress = nullptr; + std::size_t reportedSize = 0; Require(pthread_attr_setstack_nid_postfix(nullptr, &stackMarker, STACK_SIZE) == 22); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, STACK_SIZE) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(nullptr, &reportedAddress) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 22); Require(scePthreadAttrInit(&stackAttr) == SCE_OK); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, nullptr) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 0 && reportedAddress == nullptr); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, STACK_SIZE) == 0); Require(pthread_attr_setstack_nid_postfix(&stackAttr, nullptr, STACK_SIZE) == 22); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, 16383) == 22); - void* reportedAddress = nullptr; - std::size_t reportedSize = 0; Require(pthread_attr_getstack_nid_postfix(&stackAttr, &reportedAddress, &reportedSize) == 0); Require(reportedAddress == &stackMarker && reportedSize == STACK_SIZE); + reportedAddress = nullptr; + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 0 && reportedAddress == &stackMarker); Require(scePthreadAttrDestroy(&stackAttr) == SCE_OK); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, STACK_SIZE) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 22); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 90e0dc5a6..d98e85fe0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -386,7 +386,7 @@ Throughout the project, every function at every stage either **does exactly what - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [Draw input snapshots](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. -- [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing +- [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: From a36b9092d165c621820c8dc26114ed2fa771aff7 Mon Sep 17 00:00:00 2001 From: Renan Pires Date: Sun, 11 Oct 2026 13:00:35 -0300 Subject: [PATCH 103/318] feat(libc): implement the long double math functions RetroArch imports --- core/libs/prx/libc/src/Math.cpp | 16 +++++++++++ core/libs/tests/GuestMath.cpp | 51 +++++++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 68 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 23a813338..93d9b868f 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -168,6 +168,22 @@ double APS5_VABI modf_nid_postfix(double x, double* integral) { return std::modf float APS5_VABI modff_nid_postfix(float x, float* integral) { return std::modf(x, integral); } double APS5_VABI tanh_nid_postfix(double x) { return std::tanh(x); } float APS5_VABI tanhf_nid_postfix(float x) { return std::tanh(x); } +long double APS5_VABI atanl_nid_postfix(long double x) { return std::atan(x); } +long double APS5_VABI sinl_nid_postfix(long double x) { return std::sin(x); } +long double APS5_VABI cosl_nid_postfix(long double x) { return std::cos(x); } +long double APS5_VABI tanl_nid_postfix(long double x) { return std::tan(x); } +long double APS5_VABI sinhl_nid_postfix(long double x) { return std::sinh(x); } +long double APS5_VABI coshl_nid_postfix(long double x) { return std::cosh(x); } +long double APS5_VABI tanhl_nid_postfix(long double x) { return std::tanh(x); } +long double APS5_VABI expl_nid_postfix(long double x) { return std::exp(x); } +long double APS5_VABI exp2l_nid_postfix(long double x) { return std::exp2(x); } +long double APS5_VABI logl_nid_postfix(long double x) { return std::log(x); } +long double APS5_VABI log10l_nid_postfix(long double x) { return std::log10(x); } +long double APS5_VABI powl_nid_postfix(long double base, long double exp) { return std::pow(base, exp); } +long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::ldexp(x, exp); } +long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } +double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } +double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 273034aa4..88946360a 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -83,6 +83,22 @@ std::lldiv_t APS5_VABI lldiv_nid_postfix(long long, long long); std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t, std::int64_t); float APS5_VABI nanf_nid_postfix(const char*); double APS5_VABI nan_nid_postfix(const char*); +long double APS5_VABI atanl_nid_postfix(long double); +long double APS5_VABI sinl_nid_postfix(long double); +long double APS5_VABI cosl_nid_postfix(long double); +long double APS5_VABI tanl_nid_postfix(long double); +long double APS5_VABI sinhl_nid_postfix(long double); +long double APS5_VABI coshl_nid_postfix(long double); +long double APS5_VABI tanhl_nid_postfix(long double); +long double APS5_VABI expl_nid_postfix(long double); +long double APS5_VABI exp2l_nid_postfix(long double); +long double APS5_VABI logl_nid_postfix(long double); +long double APS5_VABI log10l_nid_postfix(long double); +long double APS5_VABI powl_nid_postfix(long double, long double); +long double APS5_VABI ldexpl_nid_postfix(long double, int); +long double APS5_VABI truncl_nid_postfix(long double); +double APS5_VABI logb_nid_postfix(double); +double APS5_VABI remainder_nid_postfix(double, double); } static void Require(bool value) { if (!value) std::abort(); } @@ -313,6 +329,40 @@ static void CheckAsin() { Require(!std::fetestexcept(FE_INVALID)); } +static void CheckExtendedMath() { + Require(ExtendedBits(atanl_nid_postfix(INFINITY), 0xc90fdaa22168c235ull, 0x3fff)); + Require(std::fabs(atanl_nid_postfix(1.0L) - 0.785398163397448309616L) < 1e-18L); + Require(std::fabs(sinl_nid_postfix(0.5L) - 0.479425538604203000273L) < 1e-18L); + Require(std::fabs(cosl_nid_postfix(0.5L) - 0.877582561890372716116L) < 1e-18L); + Require(std::fabs(tanl_nid_postfix(0.5L) - 0.546302489843790513255L) < 1e-18L); + Require(std::fabs(sinhl_nid_postfix(1.0L) - 1.17520119364380145688L) < 1e-18L); + Require(std::fabs(coshl_nid_postfix(1.0L) - 1.54308063481524377848L) < 1e-18L); + Require(std::fabs(tanhl_nid_postfix(0.5L) - 0.462117157260009758502L) < 1e-18L); + Require(std::fabs(expl_nid_postfix(1.0L) - 2.71828182845904523536L) < 1e-18L); + Require(std::fabs(logl_nid_postfix(2.0L) - 0.693147180559945309417L) < 1e-18L); + Require(sinl_nid_postfix(0.0L) == 0.0L && std::signbit(sinl_nid_postfix(-0.0L)) && cosl_nid_postfix(0.0L) == 1.0L); + Require(std::isnan(sinl_nid_postfix(INFINITY)) && std::isnan(cosl_nid_postfix(-INFINITY)) && std::isnan(tanl_nid_postfix(INFINITY))); + Require(sinhl_nid_postfix(-INFINITY) == -INFINITY && coshl_nid_postfix(-INFINITY) == INFINITY && tanhl_nid_postfix(INFINITY) == 1.0L); + Require(sinhl_nid_postfix(11358.0L) == INFINITY && std::isfinite(sinhl_nid_postfix(11356.0L)) && std::isfinite(coshl_nid_postfix(11356.0L))); + Require(expl_nid_postfix(0.0L) == 1.0L && expl_nid_postfix(-INFINITY) == 0.0L && expl_nid_postfix(11357.0L) == INFINITY); + Require(exp2l_nid_postfix(10.0L) == 1024.0L && exp2l_nid_postfix(-16446.0L) == 0.0L && exp2l_nid_postfix(16384.0L) == INFINITY); + Require(ExtendedBits(exp2l_nid_postfix(-16445.0L), 1, 0) && ExtendedBits(exp2l_nid_postfix(-16444.0L), 2, 0)); + Require(logl_nid_postfix(1.0L) == 0.0L && logl_nid_postfix(0.0L) == -INFINITY && std::isnan(logl_nid_postfix(-1.0L))); + Require(std::fabs(log10l_nid_postfix(1000.0L) - 3.0L) < 1e-18L && log10l_nid_postfix(1.0L) == 0.0L); + Require(log10l_nid_postfix(0.0L) == -INFINITY && std::isnan(log10l_nid_postfix(-1.0L))); + Require(powl_nid_postfix(2.0L, 64.0L) == 18446744073709551616.0L && powl_nid_postfix(-2.0L, 3.0L) == -8.0L); + Require(powl_nid_postfix(0.0L, -1.0L) == INFINITY && powl_nid_postfix(1.0L, NAN) == 1.0L && std::isnan(powl_nid_postfix(-2.0L, 0.5L))); + Require(ldexpl_nid_postfix(1.0L, 16383) == 0x1p16383L && ldexpl_nid_postfix(1.0L, 16384) == INFINITY && ldexpl_nid_postfix(3.0L, -2) == 0.75L); + Require(ExtendedBits(ldexpl_nid_postfix(1.0L, -16445), 1, 0) && ldexpl_nid_postfix(1.0L, -16446) == 0.0L); + Require(truncl_nid_postfix(2.7L) == 2.0L && truncl_nid_postfix(-2.7L) == -2.0L && ExtendedBits(truncl_nid_postfix(-0.5L), 0, 0x8000)); + Require(truncl_nid_postfix(9223372036854775807.5L) == 9223372036854775807.0L && truncl_nid_postfix(-INFINITY) == -INFINITY); + Require(logb_nid_postfix(8.0) == 3.0 && logb_nid_postfix(0.75) == -1.0 && logb_nid_postfix(0x1p-1074) == -1074.0); + Require(logb_nid_postfix(0.0) == -INFINITY && logb_nid_postfix(-INFINITY) == INFINITY && std::isnan(logb_nid_postfix(NAN))); + Require(remainder_nid_postfix(5.0, 3.0) == -1.0 && remainder_nid_postfix(7.0, 2.0) == -1.0 && remainder_nid_postfix(5.0, 2.0) == 1.0); + Require(std::signbit(remainder_nid_postfix(-4.0, 2.0)) && remainder_nid_postfix(1.0, INFINITY) == 1.0); + Require(std::isnan(remainder_nid_postfix(1.0, 0.0)) && std::isnan(remainder_nid_postfix(INFINITY, 1.0))); +} + static void CheckHyperbolic() { const double infinity = std::numeric_limits::infinity(); Require(sinh_nid_postfix(0.) == 0. && std::signbit(sinh_nid_postfix(-0.))); @@ -385,6 +435,7 @@ static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) int main() { CheckFloatClassification(); CheckIntegerConversions(); + CheckExtendedMath(); Require(atof_nid_postfix(" -12.5tail") == -12.5); char* end = nullptr; const char input[] = "0x1.8p+2 remainder"; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b3fb88528..2fc9cff1a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -195,6 +195,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceImeGetPanelPositionAndForm](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills; it is never written, since no panel can be open. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does, and the lookup is Linux only, so on Windows the call throws when the thread has destructors - [acosl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) +- [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl`, `truncl`, `logb` and `remainder` are exact operations and give the console's result - [std_execute_once](../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol - [Dinkumware mutexes and conditions](../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. - [random, srandom, initstate, setstate](../../core/libs/prx/libc/src/Random.cpp) (libc) follow FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c): the additive feedback generator on a default 128-byte `TYPE_3` table, `good_rand` seeding, state sizes rounded down to 8, 32, 64, 128 or 256 bytes, and `setstate` returning the previous state or null for a rear position past the degree. `initstate` below 8 bytes returns null without setting errno, as that file and its manual do (glibc sets `EINVAL`). FreeBSD does not lock the generator; here every call is serialised by one lock, so concurrent callers draw the serial sequence between them. A null state throws, where FreeBSD faults. That the console's `libSceLibcInternal` uses this FreeBSD code and not another generator is unverified From 7663d8479cceb3d6fa7f1b9c10e88873966255f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 18:03:18 +0200 Subject: [PATCH 104/318] docs(technical-debt): note glibc errno for logb and remainder --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2fc9cff1a..c0f18c562 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -195,7 +195,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceImeGetPanelPositionAndForm](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills; it is never written, since no panel can be open. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does, and the lookup is Linux only, so on Windows the call throws when the thread has destructors - [acosl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) -- [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl`, `truncl`, `logb` and `remainder` are exact operations and give the console's result +- [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception - [std_execute_once](../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol - [Dinkumware mutexes and conditions](../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. - [random, srandom, initstate, setstate](../../core/libs/prx/libc/src/Random.cpp) (libc) follow FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c): the additive feedback generator on a default 128-byte `TYPE_3` table, `good_rand` seeding, state sizes rounded down to 8, 32, 64, 128 or 256 bytes, and `setstate` returning the previous state or null for a rear position past the degree. `initstate` below 8 bytes returns null without setting errno, as that file and its manual do (glibc sets `EINVAL`). FreeBSD does not lock the generator; here every call is serialised by one lock, so concurrent callers draw the serial sequence between them. A null state throws, where FreeBSD faults. That the console's `libSceLibcInternal` uses this FreeBSD code and not another generator is unverified From db49377bef0f2663462d4df451c8c035a823b8b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 14:32:58 +0000 Subject: [PATCH 105/318] fix(libkernel): return SCE error codes from sceKernelFsync sceKernelFsync returned the host fsync or _commit result, so a failure returned -1 instead of an SCE error code, and a guest socket descriptor reached the host call. fsync then set the guest errno from the host errno rather than from that result. Handle sockets and return SCE error codes as sceKernelFdatasync does (EINVAL for an open socket, EBADF for a closed one or an unknown descriptor), and have fsync take the errno from sceKernelFsync's result like the other POSIX wrappers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libkernel/File/src/Stdio.cpp | 9 +++++---- core/libs/tests/GuestFilesystem.cpp | 8 ++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 51dfc6830..feac9dc15 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -725,11 +725,13 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { } int APS5_VABI sceKernelFsync(int fd) { + if (fd >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(fd) ? GUEST_EINVAL : GUEST_EBADF); #ifdef _WIN32 - return NativeSync(fd); + if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else - return ::fsync(fd); + if (::fsync(fd) != 0) return SceErrorFromErrno(errno); #endif + return 0; } int APS5_VABI sceKernelFdatasync(int fd) { @@ -1131,8 +1133,7 @@ int APS5_VABI futimes_nid_postfix(int d, const KernelTimeval* times) { } int APS5_VABI fsync_nid_postfix(int fd) { - if (sceKernelFsync(fd) != 0) return PosixFailure(errno); - return 0; + return PosixResult(sceKernelFsync(fd)); } int APS5_VABI fdatasync_nid_postfix(int fd) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index acadafb4c..4dbe5fbb3 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -30,6 +30,7 @@ int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI sceKernelFsync(int); int APS5_VABI fdatasync_nid_postfix(int); +int APS5_VABI fsync_nid_postfix(int); int APS5_VABI sceKernelFdatasync(int); int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t*); int APS5_VABI sceKernelFtruncate(int, long long); @@ -358,6 +359,8 @@ int main() { Require(futimes_nid_postfix(descriptor, nullptr) == -1 && *__error_nid_postfix() == 9); Require(fdatasync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFdatasync(descriptor) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFsync(descriptor) == static_cast(0x80020009u)); const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); Require(sceKernelFchmod(socket, 0600) == static_cast(0x80020016u)); @@ -365,12 +368,17 @@ int main() { Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 22); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); Require(sceKernelFdatasync(socket) == static_cast(0x80020016u)); + Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelFsync(socket) == static_cast(0x80020016u)); Require(close_nid_postfix(socket) == 0); Require(fchmod_nid_postfix(socket, 0600) == -1 && *__error_nid_postfix() == 9); Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 9); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFdatasync(socket) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFsync(socket) == static_cast(0x80020009u)); Require(sceKernelFdatasync(0x7fffffff) == static_cast(0x80020009u)); + Require(sceKernelFsync(0x7fffffff) == static_cast(0x80020009u)); Require(remove_nid_postfix(sized.string().c_str()) == 0); const auto present = root / "present.txt"; const auto presentName = present.string(); From 092f7ef00afa4b1e7c692ab41d9865d51fe82799 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:56:06 +0200 Subject: [PATCH 106/318] feat(libkernel): implement the pthread spinlock calls Add pthread_spin_init (ZMn3clnAGBA), pthread_spin_destroy (IJIggoPZExk), pthread_spin_lock (pw+70ClLYlY), pthread_spin_trylock (rCTGkBIHfPY) and pthread_spin_unlock (LEfMMCT+SlM). The NIDs were computed with tools/nid_names.py and match aerolib.csv. libkernel has no scePthreadSpin* twin in aerolib, so the calls are implemented directly, like the barrier. Titles: Porpoise PPSA99764 and RetroArch PPSA99169 import pthread_spin_lock. Predicted from relink registry order, not observed: after pthread_cond_reltimedwait_np, Porpoise stops with "FAIL: unresolved ELF import pw+70ClLYlY from libkernel.prx" (registry index 387; RetroArch index 493). No call site is resolved in the registry. Behaviour follows FreeBSD 11.4 libthr thr_pspinlock.c and thr_umtx.h (8c639945a44588f6cfdddfb057bb5aad88735da0). The guest pthread_spinlock_t holds a pointer to an allocated lock. EINVAL for a null or destroyed lock and for a pshared other than PTHREAD_PROCESS_PRIVATE or PTHREAD_PROCESS_SHARED, EBUSY from trylock while any thread holds the lock, EPERM from unlock by a thread that does not hold it. libthr spins forever when the holder locks again; that call throws here. A shared lock is allocated like a private one, since guest code runs in one process. Recorded in TechnicalDebt. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 13ebf7f926fa2ee1c825cb1fcf18451a8781c045) --- core/libs/prx/libkernel/CMakeLists.txt | 11 ++ .../prx/libkernel/Pthread/Posix/Spinlock.cpp | 76 +++++++++++ core/libs/tests/GuestPthreadSpin.cpp | 122 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 210 insertions(+) create mode 100644 core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp create mode 100644 core/libs/tests/GuestPthreadSpin.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6fa23e5fe..c3e10c731 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Once.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp @@ -314,6 +315,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_self PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_spin_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSpin.cpp) + target_include_directories(guest_pthread_spin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_spin_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_spin_tests) + add_test(NAME guest_pthread_spin COMMAND guest_pthread_spin_tests) + set_tests_properties(guest_pthread_spin PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_spin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_getname_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadGetname.cpp) target_include_directories(guest_pthread_getname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_getname_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp new file mode 100644 index 000000000..3990f0d29 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp @@ -0,0 +1,76 @@ +#include "SceTypes.hpp" +#include "Common.hpp" +#include +#include +#include +#include + +struct PthreadSpinlockPrivate { + std::atomic owner{}; +}; + +namespace { + +constexpr int GUEST_EPERM = 1; +constexpr int GUEST_ENOMEM = 12; +constexpr int GUEST_EBUSY = 16; +constexpr int PROCESS_PRIVATE = 0; +constexpr int PROCESS_SHARED = 1; +constexpr int SPIN_COUNT = 100000; + +bool tryAcquire(PthreadSpinlockPrivate* spinlock) { + std::thread::id unowned{}; + return spinlock->owner.compare_exchange_strong(unowned, std::this_thread::get_id(), std::memory_order_acquire, std::memory_order_relaxed); +} + +} + +extern "C" { + +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlockPrivate** lock, int pshared) { + if (!lock) return PosixThread::GUEST_EINVAL; + if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + auto* spinlock = new (std::nothrow) PthreadSpinlockPrivate; + if (!spinlock) return GUEST_ENOMEM; + *lock = spinlock; + return 0; +} + +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + delete *lock; + *lock = nullptr; + return 0; +} + +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + return tryAcquire(*lock) ? 0 : GUEST_EBUSY; +} + +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto* spinlock = *lock; + if (spinlock->owner.load(std::memory_order_relaxed) == std::this_thread::get_id()) + throw std::runtime_error("pthread_spin_lock: the calling thread already holds the lock and would spin forever"); + int count = SPIN_COUNT; + while (!tryAcquire(spinlock)) { + while (spinlock->owner.load(std::memory_order_relaxed) != std::thread::id{}) { + if (--count <= 0) { + count = SPIN_COUNT; + std::this_thread::yield(); + } + } + } + return 0; +} + +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto owner = std::this_thread::get_id(); + if (!(*lock)->owner.compare_exchange_strong(owner, std::thread::id{}, std::memory_order_release, std::memory_order_relaxed)) + return GUEST_EPERM; + return 0; +} + +} diff --git a/core/libs/tests/GuestPthreadSpin.cpp b/core/libs/tests/GuestPthreadSpin.cpp new file mode 100644 index 000000000..9783df2dd --- /dev/null +++ b/core/libs/tests/GuestPthreadSpin.cpp @@ -0,0 +1,122 @@ +#include "SceTypes.hpp" +#include +#include + +struct PthreadSpinlockPrivate; +using PthreadSpinlock = PthreadSpinlockPrivate*; + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlock* lock, int pshared); +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlock* lock); +} + +static constexpr int GUEST_EPERM = 1; +static constexpr int GUEST_EBUSY = 16; +static constexpr int GUEST_EINVAL = 22; +static constexpr int PTHREAD_PROCESS_PRIVATE = 0; +static constexpr int PTHREAD_PROCESS_SHARED = 1; +static constexpr int Increments = 200000; + +static void Require(bool value) { if (!value) std::abort(); } + +struct Shared { + PthreadSpinlock lock = nullptr; + long counter = 0; + int trylock = -1; + int unlock = -1; +}; + +static void* APS5_VABI Contender(void* arg) { + auto& shared = *static_cast(arg); + shared.trylock = pthread_spin_trylock_nid_postfix(&shared.lock); + shared.unlock = pthread_spin_unlock_nid_postfix(&shared.lock); + return nullptr; +} + +static void* APS5_VABI Incrementer(void* arg) { + auto& shared = *static_cast(arg); + for (int i = 0; i < Increments; ++i) { + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + ++shared.counter; + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + } + return nullptr; +} + +static void ArgumentErrors() { + Require(pthread_spin_init_nid_postfix(nullptr, PTHREAD_PROCESS_PRIVATE) == GUEST_EINVAL); + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, 2) == GUEST_EINVAL); + Require(pthread_spin_init_nid_postfix(&lock, -1) == GUEST_EINVAL); + Require(lock == nullptr); + Require(pthread_spin_lock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_lock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void SingleThread(int pshared) { + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, pshared) == 0); + Require(lock != nullptr); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_lock_nid_postfix(&lock) == 0); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EBUSY); + bool threw = false; + try { + pthread_spin_lock_nid_postfix(&lock); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_trylock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&lock) == 0); + Require(lock == nullptr); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void HeldByAnotherThread() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Contender, &shared, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(shared.trylock == GUEST_EBUSY); + Require(shared.unlock == GUEST_EPERM); + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +static void MutualExclusion() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Pthread first = nullptr; + Pthread second = nullptr; + Require(scePthreadCreate(&first, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadCreate(&second, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadJoin(first, nullptr) == 0); + Require(scePthreadJoin(second, nullptr) == 0); + Require(shared.counter == 2L * Increments); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +int main() { + ArgumentErrors(); + SingleThread(PTHREAD_PROCESS_PRIVATE); + SingleThread(PTHREAD_PROCESS_SHARED); + HeldByAnotherThread(); + MutualExclusion(); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6fde01523..f659bc491 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -625,3 +625,4 @@ Throughout the project, every function at every stage either **does exactly what - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well +- [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws From 774f21845dff20abddcdb02dabd4b9a6192c8ce3 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:58:03 +0200 Subject: [PATCH 107/318] feat(libkernel): implement pthread_cond_reltimedwait_np Add pthread_cond_reltimedwait_np (K953PF5u6Pc; computed with tools/nid_names.py and matching aerolib.csv) as scePthreadCondTimedwait with POSIX error numbers: ETIMEDOUT when the relative timeout expires, EPERM when the caller does not hold the mutex, and the mutex held again on return in every waiting case. Titles: Porpoise PPSA99764 and RetroArch PPSA99169 import it. Predicted from relink registry order, not observed: Porpoise stops with "FAIL: unresolved ELF import K953PF5u6Pc from libkernel.prx" (registry index 384; RetroArch index 490). No call site is resolved in the registry. The third argument is a relative timeout in microseconds, not a struct timespec. FreeBSD 11.4 libthr (8c639945a44588f6cfdddfb057bb5aad88735da0) has no such function. shadPS4 (3a9f57265d28975658f1ca941daa5c879c7a74bd, src/core/libraries/kernel/threads/condvar.cpp:229-234, 445, 467-468) registers posix_pthread_cond_reltimedwait_np(cond, mutex, u64 usec) for this NID and the same function for scePthreadCondTimedwait (BmMjYxmew1w), whose microsecond argument this repository already implements. The width follows the SCE twin's 32-bit SceKernelUseconds, as sem_reltimedwait_np does, not shadPS4's u64. There is no timespec to validate, so no EINVAL case. Recorded in TechnicalDebt. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit c86ec8417d12f5b12136b0f06eae06493b0e7a62) --- .../libs/prx/libkernel/Pthread/Posix/Cond.cpp | 4 ++ core/libs/tests/GuestCondTimedwait.cpp | 67 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 72 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index b9a7b211c..5c5e64d18 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -41,6 +41,10 @@ int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex return toPosix(CondOperations::AbsoluteTimedwait(cond, mutex, abstime)); } +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { + return toPosix(scePthreadCondTimedwait(cond, mutex, usec)); +} + int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex) { return toPosix(scePthreadCondWait(cond, mutex)); } diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 310812871..5a9190e1e 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include #include @@ -10,9 +11,12 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); +int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *)(void*), void*, void*); void APS5_VABI __pthread_cxa_finalize_nid_postfix(void*); unsigned int APS5_VABI sceKernelSleep(unsigned int seconds); @@ -23,6 +27,9 @@ static void APS5_VABI Cleanup(void*) { cleanupAt = 0; } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); +static constexpr int POSIX_EPERM = 1; +static constexpr int POSIX_ETIMEDOUT = 60; static void Require(bool value) { if (!value) std::abort(); } @@ -39,7 +46,67 @@ static void* APS5_VABI Contender(void* arg) { return nullptr; } +struct Signalled { + PthreadMutex mutex = nullptr; + PthreadCond cond = nullptr; + bool ready = false; + int trylock = SCE_OK; +}; + +static void* APS5_VABI Trylocker(void* arg) { + auto& signalled = *static_cast(arg); + signalled.trylock = scePthreadMutexTrylock(&signalled.mutex); + return nullptr; +} + +static void* APS5_VABI Signaller(void* arg) { + auto& signalled = *static_cast(arg); + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + signalled.ready = true; + Require(scePthreadCondSignal(&signalled.cond) == SCE_OK); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + return nullptr; +} + +static void RequireHeldByCaller(Signalled& signalled) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Trylocker, &signalled, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(signalled.trylock == SCE_KERNEL_ERROR_EBUSY); +} + +static void RelativeWait() { + Signalled signalled; + Require(scePthreadMutexInit(&signalled.mutex, nullptr, nullptr) == SCE_OK); + Require(scePthreadCondInit(&signalled.cond, nullptr, nullptr) == SCE_OK); + + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 1000) == POSIX_EPERM); + + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + const auto start = std::chrono::steady_clock::now(); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 50000) == POSIX_ETIMEDOUT); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + RequireHeldByCaller(signalled); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 0) == POSIX_ETIMEDOUT); + RequireHeldByCaller(signalled); + + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Signaller, &signalled, nullptr) == SCE_OK); + const auto waitStart = std::chrono::steady_clock::now(); + while (!signalled.ready) + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 10000000) == 0); + Require(std::chrono::steady_clock::now() - waitStart < std::chrono::seconds(5)); + RequireHeldByCaller(signalled); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + Require(scePthreadCondDestroy(&signalled.cond) == SCE_OK); + Require(scePthreadMutexDestroy(&signalled.mutex) == SCE_OK); +} + int main() { + RelativeWait(); + Context context; PthreadCond cond = nullptr; Require(scePthreadMutexInit(&context.mutex, nullptr, nullptr) == SCE_OK); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f659bc491..ab0e8c98a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -626,3 +626,4 @@ Throughout the project, every function at every stage either **does exactly what - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws +- [pthread_cond_reltimedwait_np](../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console From 89351601607f8ca85f8288875e27f20a50fae85b Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 15:23:14 +0000 Subject: [PATCH 108/318] fix(libkernel): handle available sem_timedwait counts first Co-authored-by: milescoviello --- core/libs/prx/libkernel/Pthread/Posix/Sem.cpp | 8 ++++ core/libs/tests/GuestPosixSem.cpp | 41 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 3 ++ 3 files changed, 52 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index e71072e2a..d11bda07a 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -84,6 +84,14 @@ int APS5_VABI sem_trywait_nid_postfix(void* sem) { int APS5_VABI sem_timedwait_nid_postfix(void* sem, const KernelTimespec* abstime) { auto* semaphore = Get(sem); if (!semaphore) return Fail(PosixThread::GUEST_EINVAL); + { + std::lock_guard lock(semaphore->lock); + if (semaphore->count > 0) { + --semaphore->count; + return 0; + } + } + if (!abstime) return WaitFor(semaphore, nullptr); KernelUseconds usec = 0; if (!PosixThread::RelativeMicroseconds(GUEST_REALTIME_CLOCK, abstime, &usec)) return Fail(PosixThread::GUEST_EINVAL); return WaitFor(semaphore, &usec); diff --git a/core/libs/tests/GuestPosixSem.cpp b/core/libs/tests/GuestPosixSem.cpp index aae90c1ae..f6a296312 100644 --- a/core/libs/tests/GuestPosixSem.cpp +++ b/core/libs/tests/GuestPosixSem.cpp @@ -1,16 +1,20 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include #include +#include extern "C" { int APS5_VABI sem_init_nid_postfix(void*, int, unsigned int); int APS5_VABI sem_destroy_nid_postfix(void*); int APS5_VABI sem_post_nid_postfix(void*); int APS5_VABI sem_trywait_nid_postfix(void*); +int APS5_VABI sem_timedwait_nid_postfix(void*, const KernelTimespec*); int APS5_VABI sem_getvalue_nid_postfix(void*, int*); int* APS5_VABI __error_nid_postfix(); } @@ -58,4 +62,41 @@ int main(int argc, char** argv) { Require(sem_trywait_nid_postfix(&sem) == 0, "normal wait must succeed"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "normal wait must decrement"); Require(sem_destroy_nid_postfix(&sem) == 0, "normal destroy must succeed"); + + KernelTimespec invalidTimeout{0, 1000000000}; + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "available semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, nullptr) == 0, "available semaphore must ignore null timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "null-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "invalid-timeout semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == 0, "available semaphore must ignore invalid timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "invalid-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "invalid-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "blocked semaphore must initialize"); + std::atomic entered = false; + std::atomic completed = false; + int waitResult = -2; + std::thread waiter([&] { + entered.store(true, std::memory_order_release); + waitResult = sem_timedwait_nid_postfix(&sem, nullptr); + completed.store(true, std::memory_order_release); + }); + while (!entered.load(std::memory_order_acquire)) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(!completed.load(std::memory_order_acquire), "blocked null-timeout wait must remain blocked"); + Require(sem_post_nid_postfix(&sem) == 0, "post must release a null-timeout waiter"); + waiter.join(); + Require(waitResult == 0, "null-timeout waiter must succeed after a post"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout waiter must consume the posted count"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "invalid-timeout blocked semaphore must initialize"); + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == -1 && *__error_nid_postfix() == 22, "blocked invalid-timeout wait must report EINVAL"); + KernelTimespec expiredTimeout{0, 0}; + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &expiredTimeout) == -1 && *__error_nid_postfix() == 60, "blocked expired-timeout wait must report ETIMEDOUT"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "failed timed waits must preserve the count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "blocked semaphore destroy must succeed"); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c699ad506..fccc07514 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -357,6 +357,9 @@ Throughout the project, every function at every stage either **does exactly what ### Functional - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. + +- [sem_timedwait](../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. + - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [ftrylockfile](../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it From 5164f7875a4c5b9e73f117402540c9096f884b27 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 09:42:03 +0200 Subject: [PATCH 109/318] feat(libSceVoiceQoS): implement sceVoiceQoSEnd End clears the init flag set by sceVoiceQoSInit and returns 0, so the library can be initialized again. Before an initialization it returns 0x804E0901 (LIBVOICEQOS_NOT_INIT), as measured on a PS5 (firmware 13.60) in #1407. LEGO Horizon Adventures (PPSA14632) calls it at shutdown and Ghost of Yotei (PPSA26344) calls it on its sceVoiceQoSInit failure branch. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 25fbc944faddc5fb3519c4ce4b8d3600b3bc9e1b) --- core/libs/prx/libSceVoiceQoS/Export.cpp | 7 +++++-- core/libs/tests/GuestVoiceQoS.cpp | 10 ++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 4ab9fb411..4756143ee 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -9,6 +9,7 @@ namespace { constexpr std::int32_t VOICE_QOS_APP_TYPE_GAME = 0x20000000; constexpr std::int32_t VOICE_QOS_APP_TYPE_10000000 = 0x10000000; constexpr std::uint32_t VOICE_QOS_MEMORY_SIZE = 0x40000; +constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT = static_cast(0x804E0901); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID = static_cast(0x804E0902); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED = static_cast(0x804E0905); @@ -28,8 +29,10 @@ int APS5_VABI sceVoiceQoSInit(void* mem_block, uint32_t mem_size, int32_t app_ty } int APS5_VABI sceVoiceQoSEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; + g_initialized = false; + return 0; } int APS5_VABI sceVoiceQoSCreateLocalEndpoint() { diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index bf8b3467c..44aa99634 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -5,6 +5,7 @@ extern "C" { int APS5_VABI sceVoiceQoSInit(void*, std::uint32_t, std::int32_t); +int APS5_VABI sceVoiceQoSEnd(); } static void Require(bool value) { if (!value) std::abort(); } @@ -13,8 +14,10 @@ int main(int argc, char** argv) { Require(argc == 2); constexpr int argumentInvalid = static_cast(0x804E0902); constexpr int initialized = static_cast(0x804E0905); + constexpr int notInitialized = static_cast(0x804E0901); const auto appType = static_cast(std::strtoul(argv[1], nullptr, 16)); std::vector memory(0x40000, 0xAA); + Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); Require(sceVoiceQoSInit(nullptr, 0, appType) == argumentInvalid); for (std::uint32_t size : {0u, 1u, 0x100u, 0x3FFFFu, 0x40001u, 0x80000u, 0xFFFFFFFFu}) Require(sceVoiceQoSInit(memory.data(), size, appType) == argumentInvalid); @@ -25,4 +28,11 @@ int main(int argc, char** argv) { Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); Require(sceVoiceQoSInit(nullptr, 0, 0) == initialized); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); + Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..fbb845c7a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -629,6 +629,7 @@ Throughout the project, every function at every stage either **does exactly what - [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [fstatvfs](../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) +- [sceVoiceQoSEnd](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; no endpoint is implemented - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well From 1b7e842b9c446cb8ccdb523b812b5e7e23092c2b Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:59:28 +0200 Subject: [PATCH 110/318] fix(exports): export the PS VR2 calls PPSA07411 imports that no library has PPSA07411 (Resident Evil 4) fails to load without these imports. Their signatures are unknown, so they are declared without parameters and throw until they are implemented. Adds a TechnicalDebt entry. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceHmd2/Export.cpp | 10 ++++++++++ core/libs/prx/libScePad/Export.cpp | 15 +++++++++++++++ core/libs/prx/libSceVrTracker2/Export.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 31 insertions(+) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 9cfafc35a..db43b00f6 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -16,6 +16,16 @@ std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { return SCE_HMD2_ERROR_UNSUPPORTED_FEATURE; } +int APS5_VABI sceHmd2GetDeviceInformationByHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2Terminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2Close() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index b14f0cd70..6c25afbc6 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -35,6 +35,21 @@ int APS5_VABI scePadClose_nid_postfix(int handle) { return PAD_OK; } +int APS5_VABI scePadVrControllerSetVibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerSetVibrationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerReadCombinedState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI scePadDeviceClassGetExtendedInformation(int handle, PadDeviceClassExtendedInformation* info) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (info == nullptr) return PAD_ERROR_INVALID_ARG; diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 75a4fce48..42b258980 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -10,6 +10,11 @@ int APS5_VABI sceVrTracker2Finalize() { return 0; } +int APS5_VABI sceVrTracker2ResetLocalCoordinate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..5a698d42d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -626,6 +626,7 @@ Throughout the project, every function at every stage either **does exactly what - PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, sceSslConnect, sceSslRead, sceSslWrite](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. +- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2GetDeviceInformationByHandle and sceHmd2Terminate](../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) - [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [fstatvfs](../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) From c154b8bff38056acbfbba3171abfb156e53e627d Mon Sep 17 00:00:00 2001 From: Hannah Lyons <7751607+babyhuey@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:01:58 -0400 Subject: [PATCH 111/318] feat(libkernel_sys): implement openat --- .../File/include/DirectoryDescriptor.hpp | 1 + core/libs/prx/libkernel/File/src/Open.cpp | 35 ++++++++++++------- core/libs/prx/libkernel_sys/Export.cpp | 28 +++++++++++++++ core/libs/tests/GuestKernelSys.cpp | 26 ++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 79 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 2031b6c4c..a8ce99649 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -6,6 +6,7 @@ #include extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* error); +extern "C" int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode); namespace File { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index bfcf004c5..caae32d96 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -185,18 +185,7 @@ static int SceIoErrorFromErrno(int error) { return static_cast(0x80020000u | static_cast(guest)); } -extern "C" { - -int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { - if (path != nullptr && *path == '\0') return SCE_KERNEL_ERROR_ENOENT; - APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); - if (path != nullptr && File::IsRandomDevicePath(path)) { - if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY) throw std::runtime_error(std::string(__func__) + ": writing to " + path + " is not implemented"); - const int fd = File::OpenRandomDevice(); - return fd < 0 ? SceErrorFromErrno(errno) : fd; - } - std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); +static int OpenNative(const std::filesystem::path& native, int flags, std::uint16_t mode) { #ifdef _WIN32 if ((flags & (SCE_KERNEL_O_DIRECTORY | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_DIRECTORY) { std::error_code error; @@ -223,6 +212,28 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { return fd; } +extern "C" { + +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { + if (path != nullptr && *path == '\0') return SCE_KERNEL_ERROR_ENOENT; + APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); + if (path != nullptr && File::IsRandomDevicePath(path)) { + if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY) throw std::runtime_error(std::string(__func__) + ": writing to " + path + " is not implemented"); + const int fd = File::OpenRandomDevice(); + return fd < 0 ? SceErrorFromErrno(errno) : fd; + } + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); + return OpenNative(native, flags, mode); +} + +int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode) { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(directory, &error); + if (base == nullptr) return SceErrorFromErrno(error); + return OpenNative(std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)), flags, mode); +} + int APS5_VABI sceKernelClose(int d) { File::ForgetRandomDevice(d); #ifdef _WIN32 diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index f3554d51e..ffe9d557d 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -17,6 +18,7 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libc/include/CreationMask.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -43,6 +45,7 @@ constexpr int GuestEloop = 62; constexpr int GuestEnametoolong = 63; constexpr int GuestEnotempty = 66; constexpr int GuestEoverflow = 84; +constexpr int GuestAtFdcwd = -100; std::int64_t Fail(int error) { *__error_nid_postfix() = error; @@ -180,4 +183,29 @@ int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { return 0; } +int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); + +int APS5_VABI openat_nid_postfix(int fd, const char* path, int flags, ...) { + int mode = 0; + if (flags & SCE_KERNEL_O_CREAT) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, flags); + mode = __builtin_va_arg(arguments, int); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, flags); + mode = va_arg(arguments, int); + va_end(arguments); +#endif + } + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (fd == GuestAtFdcwd || *path == '/') return open_nid_postfix(path, flags, mode); + const int result = OpenInDirectory_nid_no_patch(fd, path, flags, static_cast(mode)); + if (result < 0) return static_cast(Fail(result & 0xffff)); + return result; +} + } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 2603b7db9..8803d085c 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -31,6 +31,8 @@ int APS5_VABI sceKernelClose(int); int APS5_VABI setsid_nid_postfix(void); int APS5_VABI link_nid_postfix(const char*, const char*); int APS5_VABI symlink_nid_postfix(const char*, const char*); +int APS5_VABI openat_nid_postfix(int, const char*, int, ...); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); int* APS5_VABI __error_nid_postfix(); } @@ -175,6 +177,30 @@ int main() { Require(symlink_nid_postfix("original.bin", (root / "missing" / "symbolic").string().c_str()) == -1 && Error() == 2); Require(symlink_nid_postfix(nullptr, symbolic.c_str()) == -1 && Error() == 14); Require(symlink_nid_postfix("original.bin", "") == -1 && Error() == 2); + const int base = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(base >= 0); + const int relative = openat_nid_postfix(base, "file.txt", SCE_KERNEL_O_RDONLY); + char byte = 0; + Require(relative >= 0 && sceKernelRead(relative, &byte, 1) == 1 && byte == 'x'); + Require(sceKernelClose(relative) == 0); + const int made = openat_nid_postfix(base, "made.bin", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0644); + Require(made >= 0 && std::filesystem::is_regular_file(root / "made.bin")); + Require(sceKernelClose(made) == 0); + Require(openat_nid_postfix(base, "made.bin", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0644) == -1 && Error() == 17); + Require(openat_nid_postfix(base, "missing.bin", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); + const auto guestFile = "/" + root.filename().string() + "/file.txt"; + const int absolute = openat_nid_postfix(-1, guestFile.c_str(), SCE_KERNEL_O_RDONLY); + Require(absolute >= 0 && sceKernelClose(absolute) == 0); + const int current = openat_nid_postfix(-100, file.c_str(), SCE_KERNEL_O_RDONLY); + Require(current >= 0 && sceKernelClose(current) == 0); + const int plain = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(plain >= 0); + Require(openat_nid_postfix(plain, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 20); + Require(sceKernelClose(plain) == 0); + Require(openat_nid_postfix(-1, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 9); + Require(openat_nid_postfix(base, nullptr, SCE_KERNEL_O_RDONLY) == -1 && Error() == 14); + Require(openat_nid_postfix(base, "", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); + Require(sceKernelClose(base) == 0); std::filesystem::remove_all(root); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..d4a6b3ead 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -374,6 +374,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [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. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and 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 - [Windows file identity](../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - On Linux, [direct memory](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [Per-face depth bias](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. From 6ce1240aba9d870cc2935015e6857d1783acadf8 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 11:07:42 +0200 Subject: [PATCH 112/318] feat(libSceContentExport): implement the export session calls sceContentExportStart hands out positive session ids, at most 10 live (SharpProspero MaxExecution), and sceContentExportFinish closes them. sceContentExportGetProgress, sceContentExportCancel and sceContentExportFromDataWithThumbnail take their Orbital signatures and check the session. The host has no media library to export into, so FromDataWithThumbnail throws before it calls either data callback, and GetProgress and Cancel throw for a live session since no export ever runs and their result for an idle session is unknown. An eleventh Start and Term with live sessions throw. Titles: Cult of the Lamb PPSA06465 imports GetProgress, Cancel and FromDataWithThumbnail (#2686); PPSA21564 (#271) and Cyberpunk 2077 PPSA04029 (#266) import Start. Sources: Orbital tools/ida/db_types.json (cc408367) for the signatures, SharpProspero Interop/Content/ContentExport.cs (5dfdb4b0) for the session model and MaxExecution, shadPS4 content_export.h (443f0224) for the parameter and callback layouts. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 473d6dd355d2c0ee22f9792927e0e465846569cf) --- core/libs/SceTypes.hpp | 13 +++++ .../prx/libSceContentExport/CMakeLists.txt | 2 +- core/libs/prx/libSceContentExport/Export.cpp | 52 ++++++++++++++----- core/libs/tests/GuestContentExport.cpp | 50 +++++++++++++++++- core/libs/tests/GuestContentUnimplemented.cpp | 6 --- docs/dev/TechnicalDebt.md | 3 +- 6 files changed, 102 insertions(+), 24 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 51d48be50..36d3d47ec 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1590,6 +1590,19 @@ struct ContentExportInitParam2 { std::int64_t reserved1; }; +struct ContentExportParam { + char title[257]; + char comment[257]; + char content_type[65]; +}; +static_assert(sizeof(ContentExportParam) == 579); + +struct ContentExportCallbackParam { + std::size_t content_length; + void* func; + void* user_data; +}; + struct ContentSearchInitParam { std::size_t memory_size; }; struct PngEncCreateParam { diff --git a/core/libs/prx/libSceContentExport/CMakeLists.txt b/core/libs/prx/libSceContentExport/CMakeLists.txt index 7efbba37e..59e62f64b 100644 --- a/core/libs/prx/libSceContentExport/CMakeLists.txt +++ b/core/libs/prx/libSceContentExport/CMakeLists.txt @@ -15,7 +15,7 @@ if(BUILD_TESTING) add_test_executable(guest_content_unimplemented_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentUnimplemented.cpp) target_include_directories(guest_content_unimplemented_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentExport libSceContentSearch libSceContentDelete libc) + target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentSearch libSceContentDelete libc) configure_windows_unwind(guest_content_unimplemented_tests) add_test(NAME guest_content_unimplemented COMMAND guest_content_unimplemented_tests) if(WIN32) diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index a626f6a42..f618daeea 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -3,11 +3,23 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include +#include +#include #include #include namespace { +constexpr std::size_t MaxExecution = 10; std::atomic g_initialized{false}; +std::mutex g_sessionMutex; +std::set g_sessions; +std::int32_t g_lastSessionId = 0; + +void RequireSession(const char* function, std::int32_t expId) { + if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); + if (!g_sessions.contains(expId)) throw std::invalid_argument(std::string(function) + ": unknown export session"); +} } extern "C" { @@ -21,9 +33,11 @@ int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param) { return 0; } -int APS5_VABI sceContentExportFinish(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportFinish(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + g_sessions.erase(expId); + return 0; } int APS5_VABI sceContentExportFromData(void) { @@ -42,26 +56,36 @@ int APS5_VABI sceContentExportFromFileWithThumbnail(void) { } int APS5_VABI sceContentExportStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_sessionMutex); + if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (g_sessions.size() >= MaxExecution) throw std::runtime_error(std::string(__func__) + ": behaviour beyond 10 live export sessions is unknown"); + if (g_lastSessionId == std::numeric_limits::max()) throw std::runtime_error(std::string(__func__) + ": export session ids exhausted"); + g_sessions.insert(++g_lastSessionId); + return g_lastSessionId; } -int APS5_VABI sceContentExportGetProgress(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportGetProgress(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": progress of a session with no running export is unknown"); } -int APS5_VABI sceContentExportCancel(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportCancel(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": cancelling a session with no running export is unknown"); } -int APS5_VABI sceContentExportFromDataWithThumbnail(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, + const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": the host has no media library to export into"); } int APS5_VABI sceContentExportTerm(void) { + std::lock_guard lock(g_sessionMutex); + if (!g_sessions.empty()) throw std::runtime_error(std::string(__func__) + ": terminating with live export sessions is unknown"); bool expected = true; if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); return 0; diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index 12038531a..ef295d6ec 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -1,25 +1,41 @@ #include "SceTypes.hpp" +#include #include +#include #include extern "C" int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param); extern "C" int APS5_VABI sceContentExportTerm(void); +extern "C" int APS5_VABI sceContentExportStart(void); +extern "C" int APS5_VABI sceContentExportFinish(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportGetProgress(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportCancel(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, + const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength); namespace { void Require(bool value) { if (!value) std::abort(); } -template +template bool Throws(TAction action) { try { action(); - } catch (const std::logic_error&) { + } catch (const TException&) { return true; } return false; } int allocator = 0; +char path[1024]{}; + +void Export(std::int32_t expId) { + ContentExportParam param{}; + ContentExportCallbackParam content{}; + ContentExportCallbackParam thumbnail{}; + sceContentExportFromDataWithThumbnail(expId, ¶m, &content, "image/png", &thumbnail, path, sizeof(path)); +} } @@ -27,6 +43,8 @@ int main() { ContentExportInitParam2 param{&allocator, &allocator, nullptr, 0x10000, 0, 0}; Require(Throws([] { sceContentExportInit2(nullptr); })); Require(Throws([] { sceContentExportTerm(); })); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportFinish(1); })); ContentExportInitParam2 missing = param; missing.free_func = nullptr; Require(Throws([&] { sceContentExportInit2(&missing); })); @@ -38,5 +56,33 @@ int main() { Require(sceContentExportTerm() == 0); Require(Throws([] { sceContentExportTerm(); })); Require(sceContentExportInit2(¶m) == 0); + + std::set sessions; + for (int i = 0; i < 10; ++i) { + int session = sceContentExportStart(); + Require(session > 0); + Require(sessions.insert(session).second); + } + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportTerm(); })); + int first = *sessions.begin(); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(path[0] == 0); + Require(sceContentExportFinish(first) == 0); + Require(Throws([&] { sceContentExportFinish(first); })); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(Throws([] { sceContentExportFinish(0); })); + Require(Throws([] { sceContentExportFinish(-1); })); + sessions.erase(first); + int reopened = sceContentExportStart(); + Require(reopened > 0 && reopened != first); + Require(sessions.insert(reopened).second); + for (int session : sessions) Require(sceContentExportFinish(session) == 0); Require(sceContentExportTerm() == 0); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([&] { sceContentExportFinish(reopened); })); } diff --git a/core/libs/tests/GuestContentUnimplemented.cpp b/core/libs/tests/GuestContentUnimplemented.cpp index 00a291f52..549946300 100644 --- a/core/libs/tests/GuestContentUnimplemented.cpp +++ b/core/libs/tests/GuestContentUnimplemented.cpp @@ -6,9 +6,6 @@ extern "C" { int APS5_VABI sceContentDeleteById(); -int APS5_VABI sceContentExportGetProgress(); -int APS5_VABI sceContentExportCancel(); -int APS5_VABI sceContentExportFromDataWithThumbnail(); int APS5_VABI sceContentSearchGetContentLastUpdateId(); int APS5_VABI sceContentSearchOpenMetadataByContentId(); int APS5_VABI sceContentSearchGetTotalContentSize(); @@ -34,9 +31,6 @@ bool ReportsNotImplemented(const char* name, int (APS5_VABI *function)()) { int main() { bool correct = true; correct &= ReportsNotImplemented("sceContentDeleteById", sceContentDeleteById); - correct &= ReportsNotImplemented("sceContentExportGetProgress", sceContentExportGetProgress); - correct &= ReportsNotImplemented("sceContentExportCancel", sceContentExportCancel); - correct &= ReportsNotImplemented("sceContentExportFromDataWithThumbnail", sceContentExportFromDataWithThumbnail); correct &= ReportsNotImplemented("sceContentSearchGetContentLastUpdateId", sceContentSearchGetContentLastUpdateId); correct &= ReportsNotImplemented("sceContentSearchOpenMetadataByContentId", sceContentSearchOpenMetadataByContentId); correct &= ReportsNotImplemented("sceContentSearchGetTotalContentSize", sceContentSearchGetTotalContentSize); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..a794fbcd8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -135,7 +135,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSaveDataCancel](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached -- [sceContentDeleteById](../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete), [sceContentExportGetProgress, sceContentExportCancel, sceContentExportFromDataWithThumbnail](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown +- [sceContentDeleteById](../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown - [sce::Json::Parser::parse with a read callback](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown - [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` @@ -408,6 +408,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries +- [Content export sessions](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified - [Runtime-loaded modules](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [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, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console From 82ac3bd93ea64ecdc3f8a7a050639f35c683c01f Mon Sep 17 00:00:00 2001 From: Mouhand Kaddo <73709175+Mouhand-Kaddo@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:00:36 +0400 Subject: [PATCH 113/318] fix(agc): skip read-only depth resummarize draws --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++++ .../src/Driver/Draw/DrawPrecheck.cpp | 1 + .../Graphics/include/State.hpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 15 +++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 26 +++++++++++++++++++ .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 21 ++++++++++++++- docs/dev/TechnicalDebt.md | 2 +- 7 files changed, 70 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6d3ef615a..b2b5719b1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1005,6 +1005,12 @@ target_include_directories(agc_driver_prepared_shaders_tests PRIVATE if(BUILD_TESTING) set_target_properties(agc_shader_mode_register_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_readonly_resummarize COMMAND agc_driver_tests --readonly-resummarize) + set_tests_properties(agc_driver_readonly_resummarize PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(agc_driver_readonly_resummarize PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests) set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp index 03a3f4533..267b6cf70 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp @@ -23,6 +23,7 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S localDevice->ColorMetadataPass(*pass); return DrawVerdict::Drawn; } + if (Graphics::DepthResummarizeDrawsNothing(queue)) return DrawVerdict::Nothing; rejected = Graphics::DepthMaintenanceRejection(queue); if (!rejected.empty()) return DrawVerdict::Rejected; static const bool traceIndirectEnabled = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 0012dbfa5..14749855f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -129,6 +129,7 @@ struct ColorMetadataPass { }; std::optional DecodeColorMetadataPass(const QueueState& queue); std::string DepthMaintenanceRejection(const QueueState& queue); +bool DepthResummarizeDrawsNothing(const QueueState& queue); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index ae2dcb330..65028c511 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -545,7 +545,22 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { return result; } +bool DepthResummarizeDrawsNothing(const QueueState& queue) { + const auto& cx = queue.context; + const auto control = find(cx, 0x000); + const auto depthControl = find(cx, 0x200); + const auto zInfo = find(cx, 0x010); + const auto stencilInfo = find(cx, 0x011); + const auto targetMask = find(cx, 0x8e); + const auto shaderMask = find(cx, 0x8f); + if (control == cx.end() || control->second != 0x10u || depthControl == cx.end() || zInfo == cx.end() || stencilInfo == cx.end() || targetMask == cx.end() || shaderMask == cx.end()) return false; + const auto depth = depthControl->second; + const auto format = zInfo->second & 3u; + return (depth & 2u) != 0 && (depth & 0xc000000cu) == 0 && depthPassThrough(depth) && (format == 1u || format == 3u) && (stencilInfo->second & 1u) == 0 && ColorWriteMask(cx) == 0 && PixelProgramSkipped(queue); +} + std::string DepthMaintenanceRejection(const QueueState& queue) { + if (DepthResummarizeDrawsNothing(queue)) return {}; const auto control = find(queue.context, 0x000); if (control == queue.context.end() || (control->second & ~0x2063u) == 0) return {}; const auto depthControl = find(queue.context, 0x200); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4e26034dc..ccce1307e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1297,6 +1297,32 @@ void depthMaintenanceTests() { auto absent = makeState(); absent.context.erase(0x000); Require(AgcDriver::Graphics::DepthMaintenanceRejection(absent).empty(), "an absent depth control produced a maintenance verdict"); + auto readOnly = makeState(); + readOnly.context[0x000] = 0x10; + readOnly.context[0x200] = 0x007007f3; + readOnly.context[0x010] = 0xa0000183; + readOnly.context[0x011] = 0x20000180; + readOnly.context[0x8e] = 0xffffffff; + readOnly.context[0x8f] = 0; + readOnly.shader.erase(0x8); + readOnly.shader.erase(0x9); + Require(AgcDriver::Graphics::DepthMaintenanceRejection(readOnly).empty(), "a resummarize draw with pass-through read-only depth and no stencil plane was rejected"); + for (const auto [offset, value] : {std::pair{0x200u, 0x007007f7u}, {0x200u, 0x007007fbu}, {0x200u, 0x007007e3u}, {0x011u, 0x20000181u}, {0x8fu, 0xfu}, {0x000u, 0x11u}, {0x010u, 0xa0000182u}}) { + auto changed = readOnly; + changed.context[offset] = value; + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(changed).empty(), "a resummarize draw with surface work or unsupported depth passed"); + } + auto pixel = readOnly; + pixel.shader[0x8] = 1; + pixel.shader[0x9] = 0; + pixel.context[0x1c4] = 0; + pixel.context[0x203] = 0x40; + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(pixel).empty(), "a resummarize draw with a running pixel stage passed"); + for (const auto offset : {0x200u, 0x010u, 0x011u, 0x8eu, 0x8fu}) { + auto missing = readOnly; + missing.context.erase(offset); + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(missing).empty(), "a resummarize draw without a required register passed"); + } } // SPI_SHADER_Z_FORMAT (0x1c4) and the export enables of DB_SHADER_CONTROL (0x203): Z export needs a diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index da1c9ba0b..ec50e84c9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -111,6 +112,17 @@ void testValidation() { AgcDriverWaitIdle_nid_postfix(); } +void testReadOnlyResummarize() { + std::vector commands; + for (const auto [offset, value] : {std::pair{0x000u, 0x10u}, {0x200u, 0x007007f3u}, {0x010u, 0xa0000183u}, {0x011u, 0x20000180u}, {0x8eu, 0xffffffffu}, {0x8fu, 0u}, {0x1c4u, 0u}, {0x203u, 0u}}) { + commands.insert(commands.end(), {0xc0016900u, offset, value}); + } + commands.insert(commands.end(), {0xc0012d00u, 3u, 2u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "read-only resummarize submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + void testClearState() { AgcDriver::QueueState graphics{{{0x20c, 1}}, {{0x10, 17}, {0x11, 23}}, {{0x242, 5}}}; const auto shader = graphics.shader; @@ -671,8 +683,14 @@ void testWorkerFailure() { } -int main() { +int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--readonly-resummarize") { + testReadOnlyResummarize(); + LibcRunShutdown_nid_postfix(); + std::puts("Read-only resummarize submission passed"); + return 0; + } const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; alignas(256) std::array rawCode{}; @@ -766,6 +784,7 @@ int main() { testHeaderWithoutProgramAddress(); testCopyIntoHostMemory(); testRegisteredFloatMode(); + testReadOnlyResummarize(); testWorkerFailure(); check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC driver submit tests passed"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..29e1b4a74 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -608,7 +608,7 @@ Throughout the project, every function at every stage either **does exactly what - [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. - [User-data callees](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. -- [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing; any other resummarize, copy or decompress draw throws +- [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [sceVideodec2CreateDecoder](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` and `sceVideodec2GetVp9PictureInfo` are not exported - [syslog](../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified. `openlog` and `closelog` are not implemented, so their identity and options are not applied From b8a1cbe42b0ff2be53f188129b00d16012b1ca74 Mon Sep 17 00:00:00 2001 From: hamb3r Date: Sun, 11 Oct 2026 18:13:56 +0300 Subject: [PATCH 114/318] fix(libc): add missing Unity engine static imports --- core/libs/prx/libSceAmpr/Export.cpp | 40 ----- core/libs/prx/libc/CMakeLists.txt | 10 ++ core/libs/prx/libc/Export.cpp | 4 + core/libs/prx/libc/src/Math.cpp | 1 + core/libs/prx/libc/src/Threads.cpp | 72 +++++++++ core/libs/tests/GuestLibcStaticImports.cpp | 169 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 5 +- 7 files changed, 260 insertions(+), 41 deletions(-) create mode 100644 core/libs/tests/GuestLibcStaticImports.cpp diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fb835934f..e26201927 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -218,16 +218,8 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v return AppendCommand(buffer, Apr::Opcode::WriteAddressFromCounterPair, Apr::WriteAddressFromCounterCommand{{}, reinterpret_cast(address), counter, counter + 1u}); } -struct Xtime { - std::int64_t sec; - std::int64_t nsec; -}; - static constexpr int ThrdSuccess = 0; static constexpr int ThrdError = 4; -static constexpr int GuestClockRealtime = 0; -static constexpr std::int64_t NanosPerSecond = 1000000000; -static constexpr std::int64_t LongestSleepSeconds = 86400; static std::atomic_flag sharedPtrSpinLock; @@ -238,22 +230,6 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle); int SignalCondition_nid_no_patch(void** handle); } -static KernelTimespec XtimeDeadline(const Xtime& time) { - constexpr auto largest = std::numeric_limits::max(); - constexpr auto smallest = std::numeric_limits::min(); - const auto carry = time.nsec / NanosPerSecond; - auto nanos = time.nsec % NanosPerSecond; - auto seconds = time.sec; - if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry)) throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); - seconds += carry; - if (nanos < 0) { - if (seconds == smallest) throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); - nanos += NanosPerSecond; - --seconds; - } - return {seconds, nanos}; -} - extern "C" { int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t value) { @@ -943,22 +919,6 @@ void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void) { while (sharedPtrSpinLock.test_and_set(std::memory_order_acquire)) std::this_thread::yield(); } -int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { - if (!target) APS5_INVALID_ARG_EX; - const auto deadline = XtimeDeadline(*target); - for (;;) { - KernelTimespec now{}; - if (clock_gettime_nid_postfix(GuestClockRealtime, &now) != 0) throw std::runtime_error("_Thrd_sleep: reading the realtime clock failed"); - if (now.tv_sec > deadline.tv_sec || (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) return 0; - KernelTimespec remaining{deadline.tv_sec - now.tv_sec, deadline.tv_nsec - now.tv_nsec}; - if (remaining.tv_nsec < 0) { - remaining.tv_nsec += NanosPerSecond; - --remaining.tv_sec; - } - remaining.tv_sec = std::min(remaining.tv_sec, LongestSleepSeconds); - if (nanosleep_nid_postfix(&remaining, nullptr) != 0) throw std::runtime_error("_Thrd_sleep: nanosleep failed"); - } -} int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { std::array state{}; diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index a0b57300c..70c2d5261 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -190,6 +190,16 @@ if(BUILD_TESTING) set_tests_properties(guest_libc_threads PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_libc_static_imports_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcStaticImports.cpp) + target_include_directories(guest_libc_static_imports_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libc_static_imports_tests PRIVATE libc) + configure_windows_unwind(guest_libc_static_imports_tests) + add_test(NAME guest_libc_static_imports COMMAND guest_libc_static_imports_tests) + set_tests_properties(guest_libc_static_imports PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libc_static_imports PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fiopen_tests ${LIBS_INCLUDE_DIR}/tests/GuestFiopen.cpp) target_include_directories(guest_fiopen_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fiopen_tests PRIVATE libc) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index be68a6cab..cf23ee77b 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -32,4 +32,8 @@ std::uint64_t APS5_VABI libcCyberUnknown08(void) { return 0; } +APS5_EXPORT("H2e8t5ScQGc", libc___cxa_finalize); +void APS5_VABI libc___cxa_finalize(void* dsoHandle) { + CxaFinalize_nid_no_patch(dsoHandle); +} } diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 23a813338..9ee4532df 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -68,6 +68,7 @@ int APS5_VABI __isfinitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0 int APS5_VABI __isnormal_nid_postfix(double x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } +float APS5_VABI nearbyintf_nid_postfix(float x) { return std::nearbyintf(x); } double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } double APS5_VABI asin_nid_postfix(double x) { diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 4565d8331..44db68abe 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -1,5 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include #include +#include #include #include #include @@ -9,9 +12,36 @@ #include #include #include +#include namespace { +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +constexpr std::int64_t nanosPerSecond = 1000000000; +constexpr std::int64_t longestSleepSeconds = 86400; + +timespec XtimeDeadline(const Xtime& time) { + constexpr auto largest = std::numeric_limits::max(); + constexpr auto smallest = std::numeric_limits::min(); + const auto carry = time.nsec / nanosPerSecond; + auto nanos = time.nsec % nanosPerSecond; + auto seconds = time.sec; + if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry)) + throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + seconds += carry; + if (nanos < 0) { + if (seconds == smallest) + throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + nanos += nanosPerSecond; + --seconds; + } + return {static_cast(seconds), static_cast(nanos)}; +} + constexpr int threadSuccess = 0; constexpr int threadNomem = 1; constexpr int threadBusy = 3; @@ -111,6 +141,24 @@ int APS5_VABI _Mtx_lock_nid_postfix(void** handle) { return threadSuccess; } +int APS5_VABI _Mtx_trylock_nid_postfix(void** handle) { + auto* mutex = ResolveMutex(handle, "_Mtx_trylock"); + const auto self = std::this_thread::get_id(); + if (mutex->owner.load(std::memory_order_acquire) == self) { + if ((mutex->type & mutexRecursive) == 0) + return threadBusy; + if (mutex->count == std::numeric_limits::max()) + throw std::overflow_error("_Mtx_trylock: recursive lock count overflow"); + ++mutex->count; + return threadSuccess; + } + if (!mutex->native.try_lock()) + return threadBusy; + mutex->owner.store(self, std::memory_order_release); + mutex->count = 1; + return threadSuccess; +} + int APS5_VABI _Mtx_unlock_nid_postfix(void** handle) { auto* mutex = ResolveMutex(handle, "_Mtx_unlock"); if (!IsOwnedByCaller(mutex)) @@ -178,4 +226,28 @@ int SignalCondition_nid_no_patch(void** handle) { return threadSuccess; } +int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { + if (!target) + APS5_INVALID_ARG_EX; + const auto deadline = XtimeDeadline(*target); + for (;;) { + timespec now{}; + if (clock_gettime(CLOCK_REALTIME, &now) != 0) + throw std::runtime_error("_Thrd_sleep: reading the realtime clock failed"); + if (now.tv_sec > deadline.tv_sec || (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) + return threadSuccess; + timespec remaining{static_cast(deadline.tv_sec - now.tv_sec), static_cast(deadline.tv_nsec - now.tv_nsec)}; + if (remaining.tv_nsec < 0) { + remaining.tv_nsec += nanosPerSecond; + --remaining.tv_sec; + } + remaining.tv_sec = std::min(remaining.tv_sec, longestSleepSeconds); + if (nanosleep(&remaining, nullptr) != 0) { + if (errno == EINTR) + continue; + throw std::runtime_error("_Thrd_sleep: nanosleep failed"); + } + } +} + } diff --git a/core/libs/tests/GuestLibcStaticImports.cpp b/core/libs/tests/GuestLibcStaticImports.cpp new file mode 100644 index 000000000..a8068575d --- /dev/null +++ b/core/libs/tests/GuestLibcStaticImports.cpp @@ -0,0 +1,169 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +extern "C" { +float APS5_VABI nearbyintf_nid_postfix(float x); +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +int APS5_VABI _Mtx_trylock_nid_postfix(void** handle); +int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime* remaining); +} + +namespace { + +void Require(bool value) { + if (!value) + std::abort(); +} + +template +void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int threadSuccess = 0; +constexpr int threadBusy = 3; +constexpr int mutexTryable = 0x02; +constexpr int mutexRecursive = 0x100; +constexpr std::int64_t nanosPerSecond = 1000000000LL; + +void TestNearbyintf() { + const float half = nearbyintf_nid_postfix(0.5f); + Require(half == 0.0f); + Require(!std::signbit(half)); + + const float negHalf = nearbyintf_nid_postfix(-0.5f); + Require(negHalf == 0.0f); + Require(std::signbit(negHalf)); + + Require(nearbyintf_nid_postfix(1.5f) == 2.0f); + Require(nearbyintf_nid_postfix(-1.5f) == -2.0f); + + Require(nearbyintf_nid_postfix(2.5f) == 2.0f); + Require(nearbyintf_nid_postfix(-2.5f) == -2.0f); + + const float negZero = nearbyintf_nid_postfix(-0.0f); + Require(negZero == 0.0f); + Require(std::signbit(negZero)); + + const float inf = std::numeric_limits::infinity(); + const float posInf = nearbyintf_nid_postfix(inf); + Require(std::isinf(posInf) && posInf > 0.0f); + + const float negInf = nearbyintf_nid_postfix(-inf); + Require(std::isinf(negInf) && negInf < 0.0f); + + const float nan = std::numeric_limits::quiet_NaN(); + Require(std::isnan(nearbyintf_nid_postfix(nan))); +} + +void TestMtxTrylock() { + void* mutex = nullptr; + Require(_Mtx_init_nid_postfix(&mutex, mutexTryable) == threadSuccess && mutex != nullptr); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&mutex) == threadBusy); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + + std::atomic held{false}; + std::atomic release{false}; + std::thread other([&] { + Require(_Mtx_lock_nid_postfix(&mutex) == threadSuccess); + held.store(true, std::memory_order_release); + while (!release.load(std::memory_order_acquire)) + std::this_thread::yield(); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + }); + + while (!held.load(std::memory_order_acquire)) + std::this_thread::yield(); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadBusy); + + release.store(true, std::memory_order_release); + other.join(); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadSuccess); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + _Mtx_destroy_nid_postfix(&mutex); + + void* recMutex = nullptr; + Require(_Mtx_init_nid_postfix(&recMutex, mutexTryable | mutexRecursive) == threadSuccess && recMutex != nullptr); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + }).join(); + + _Mtx_destroy_nid_postfix(&recMutex); +} + +void TestThrdSleep() { + timespec before{}; + Require(clock_gettime(CLOCK_REALTIME, &before) == 0); + + const std::int64_t sleepNanos = 20000000LL; + const std::int64_t targetTotal = static_cast(before.tv_sec) * nanosPerSecond + before.tv_nsec + sleepNanos; + const Xtime target{targetTotal / nanosPerSecond, targetTotal % nanosPerSecond}; + + Require(_Thrd_sleep_nid_postfix(&target, nullptr) == 0); + + timespec after{}; + Require(clock_gettime(CLOCK_REALTIME, &after) == 0); + const std::int64_t afterTotal = static_cast(after.tv_sec) * nanosPerSecond + after.tv_nsec; + Require(afterTotal >= targetTotal); + + const Xtime past{before.tv_sec - 1, before.tv_nsec}; + Require(_Thrd_sleep_nid_postfix(&past, nullptr) == 0); + + RequireThrows([] { + _Thrd_sleep_nid_postfix(nullptr, nullptr); + }); +} + +} + +int main() { + TestNearbyintf(); + TestMtxTrylock(); + TestThrdSleep(); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..a02256697 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -50,6 +50,9 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info - [sceHttpUriBuild](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. +- [_Mtx_trylock](../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. +- [nearbyintf](../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. +- [_Thrd_sleep](../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [sceCoredumpGetThreadContextInfo](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it - [sceNpWebApi2PushEventAbortHandle](../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. - [sceNpGetSandboxInfo](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. @@ -369,7 +372,7 @@ Throughout the project, every function at every stage either **does exactly what - [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes from the same thread-local host `std::random_device` as `arc4random_buf`, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws -- [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning +- [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. - [_Cnd_signal, _Cnd_init_with_name](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond From d3fc14a50c389373b5a0a174352644a25c19ef74 Mon Sep 17 00:00:00 2001 From: Aryan singh Date: Sun, 11 Oct 2026 23:26:35 +0530 Subject: [PATCH 115/318] fix(relinker): resolve debug PRX dependencies by module identity --- core/relinker/relinker/src/guest/GuestModuleBuilder.cpp | 2 +- core/relinker/relinker/tests/test_guest_module_identity.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index d51c4c160..dba9f308a 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -120,7 +120,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::map discovered; const auto matchesIdentity = [](const std::string& name, const std::vector& identities) { return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { - return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx"; + return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx" || name == identity + ".debug_prx"; }); }; for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); diff --git a/core/relinker/relinker/tests/test_guest_module_identity.py b/core/relinker/relinker/tests/test_guest_module_identity.py index 211b7a6cc..2c53e77fe 100644 --- a/core/relinker/relinker/tests/test_guest_module_identity.py +++ b/core/relinker/relinker/tests/test_guest_module_identity.py @@ -147,7 +147,7 @@ def main(): for windows in (True, False): for tag in (0x6100000d, 0x61000043): - for suffix in ('.suprx', '.prx', '.sprx'): + for suffix in ('.suprx', '.prx', '.sprx', '.debug_prx'): request = 'libGuest' + suffix result, output = convert(f'{windows}-{tag}-{suffix}', windows, request, providers={'renamed.prx': declared_provider(22, ('libGuest',), tag=tag)}, guest_request=request) From 7c1b64b0ca273c9e9f6a2812aca11f97a7b3d238 Mon Sep 17 00:00:00 2001 From: Aryan singh Date: Mon, 12 Oct 2026 03:38:07 +0530 Subject: [PATCH 116/318] fix(relinker): share module suffixes between identity and import matching The guest identity predicate accepted .debug_prx while Domain::ImportModule did not: a dependency named libGuest.debug_prx normalised to libGuest_debug_prx, so a qualified import declared in module libGuest was left unscoped. When another guest module exported the same symbol, the conversion failed with an ambiguous guest import, while .prx, .sprx and .suprx resolved to the declaring module. Module suffixes are now one list in the domain layer, used by the identity predicate, import module normalisation and the default .prx suffix. Needed-name stem aliasing keeps its own list. Co-Authored-By: Claude Opus 5.5 --- .../domain/include/domain/ImportModule.hpp | 14 ++++++++++---- .../relinker/src/guest/GuestModuleBuilder.cpp | 6 +++--- .../relinker/tests/test_guest_module_identity.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index ecc49bd39..37bab4c73 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -3,15 +3,21 @@ #include #include +#include #include #include namespace Domain { +inline constexpr std::array ModuleSuffixes = {".prx", ".sprx", ".suprx", ".debug_prx"}; + +inline std::string_view ModuleSuffix(const std::string_view name) { + const auto suffix = std::find_if(ModuleSuffixes.begin(), ModuleSuffixes.end(), [&](const auto candidate) { return name.ends_with(candidate); }); + return suffix == ModuleSuffixes.end() ? std::string_view{} : *suffix; +} + inline std::string ImportModuleName(std::string name) { - if (name.ends_with(".prx")) name.resize(name.size() - 4); - else if (name.ends_with(".sprx")) name.resize(name.size() - 5); - else if (name.ends_with(".suprx")) name.resize(name.size() - 6); + name.resize(name.size() - ModuleSuffix(name).size()); if (name.ends_with("-module")) name.resize(name.size() - 7); std::replace(name.begin(), name.end(), '.', '_'); return name; @@ -44,7 +50,7 @@ inline std::string ImportModule(const std::string& symbol, const std::map dependencies = {}) { auto name = DeclaredImportModule(symbol, modules); if (name.empty()) return {}; - if (!name.ends_with(".prx") && !name.ends_with(".sprx") && !name.ends_with(".suprx")) name += ".prx"; + if (ModuleSuffix(name).empty()) name += ".prx"; if (std::find(dependencies.begin(), dependencies.end(), name) != dependencies.end()) return name; std::string matched; for (const auto& dependency : dependencies) { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index dba9f308a..69cc82911 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -119,9 +120,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::FileReader reader; std::map discovered; const auto matchesIdentity = [](const std::string& name, const std::vector& identities) { - return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { - return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx" || name == identity + ".debug_prx"; - }); + const auto stem = std::string_view(name).substr(0, name.size() - Domain::ModuleSuffix(name).size()); + return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { return name == identity || stem == identity; }); }; for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); diff --git a/core/relinker/relinker/tests/test_guest_module_identity.py b/core/relinker/relinker/tests/test_guest_module_identity.py index 2c53e77fe..2a03efd3f 100644 --- a/core/relinker/relinker/tests/test_guest_module_identity.py +++ b/core/relinker/relinker/tests/test_guest_module_identity.py @@ -161,6 +161,20 @@ def main(): child = output.parent / 'app0/sce_module/consumer.prx.guest.prx' assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx'] + for suffix in ('.suprx', '.prx', '.sprx', '.debug_prx'): + providers = {'renamed.prx': declared_provider(22, ('libGuest',)), + 'other.prx': declared_provider(33, ('libOther',)), + 'own.prx': declared_provider(22, ('libOwn',), symbol='own'), + 'consumer.prx': consumer('libGuest' + suffix, module_name='libGuest')} + result, output = convert(f'{windows}-shared-{suffix}', windows, 'libOwn.prx', module_name='libOwn', + symbol='own#A#B', providers=providers) + assert result.returncode == 0, result.stderr + if windows: + run(output) + else: + child = output.parent / 'app0/sce_module/consumer.prx.guest.prx' + assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx'] + providers = {'one.prx': declared_provider(22, ('libGuest',)), 'two.prx': declared_provider(33, ('libGuest',), symbol='other')} result, output = convert(f'{windows}-ambiguous', windows, 'libGuest.suprx', providers=providers) From 7cf2ee844ac2c6549e5f58f69a9df4ca544d6569 Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:08:15 +0200 Subject: [PATCH 117/318] test(libSceSysmodule): verify handle and symbol resolution for libSceAudioPropagation --- core/libs/prx/libSceSysmodule/CMakeLists.txt | 4 +++- core/libs/tests/GuestSysmodule.cpp | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSysmodule/CMakeLists.txt b/core/libs/prx/libSceSysmodule/CMakeLists.txt index 5f204ccf4..b03a2433c 100644 --- a/core/libs/prx/libSceSysmodule/CMakeLists.txt +++ b/core/libs/prx/libSceSysmodule/CMakeLists.txt @@ -12,9 +12,11 @@ if(BUILD_TESTING) add_custom_target(guest_sysmodule_runtime COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "${guestSysmoduleRun}/libs/libSceFiber.prx" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" "${guestSysmoduleRun}/libs/libSceAudioPropagation.prx" VERBATIM ) - add_dependencies(guest_sysmodule_runtime libSceFiber) + add_dependencies(guest_sysmodule_runtime libSceFiber libSceAudioPropagation) add_dependencies(guest_sysmodule_tests guest_sysmodule_runtime) add_test(NAME guest_sysmodule COMMAND guest_sysmodule_tests) set_tests_properties(guest_sysmodule PROPERTIES WORKING_DIRECTORY ${guestSysmoduleRun}) diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index be4462d41..fb2567d15 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -149,9 +149,20 @@ int main() { Require(sceSysmoduleGetModuleHandleInternal(kFiberModuleId, &handle) == kModuleUnloaded); constexpr std::uint16_t kAudioPropagationModuleId = 0x0120; + std::int32_t audioPropagationHandle = -1; + Require(sceSysmoduleGetModuleHandleInternal(kAudioPropagationModuleId, &audioPropagationHandle) == kModuleUnloaded); + Require(audioPropagationHandle == -1); + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModule(kAudioPropagationModuleId) == 0); Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleGetModuleHandleInternal(kAudioPropagationModuleId, &audioPropagationHandle) == 0); + Require(audioPropagationHandle >= 0); + void* audioPropagationSymbol = nullptr; + Require(sceKernelDlsym(audioPropagationHandle, "sceAudioPropagationSystemCreate", &audioPropagationSymbol) == 0); + Require(audioPropagationSymbol != nullptr); + Require(sceSysmoduleUnloadModule(kAudioPropagationModuleId) == 0); Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == kModuleUnloaded); + Require(sceSysmoduleGetModuleHandleInternal(kAudioPropagationModuleId, &audioPropagationHandle) == kModuleUnloaded); } From 3687b2e21477b53e59e6ce0401e3699b59c12373 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:49:37 +0400 Subject: [PATCH 118/318] perf(relinker): use a flat sorted call-site index --- core/relinker/CMakeLists.txt | 16 ++++++++ .../src/analysis/CallSiteResolver.cpp | 20 +++++++--- .../relinker/tests/CallSiteIndexTests.cpp | 37 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 core/relinker/relinker/tests/CallSiteIndexTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 095915729..4e8e1a4ad 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -161,6 +161,22 @@ if(BUILD_TESTING) add_test(NAME elf_reader_bounds COMMAND elf_reader_bounds_tests) set_tests_properties(elf_reader_bounds PROPERTIES TIMEOUT 20) + add_test_executable(call_site_index_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteIndexTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_index_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME call_site_index COMMAND call_site_index_tests) + set_tests_properties(call_site_index PROPERTIES TIMEOUT 20) + add_test_executable(autorun_exit_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/AutorunExitTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 6f08ab26f..0c698568e 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -4,7 +4,8 @@ #include #include #include -#include +#include +#include namespace Relinker { @@ -70,7 +71,7 @@ public: std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize) override; private: const std::vector* _cachedTextPtr = nullptr; - std::multimap _cachedTargetToInstr; + std::vector> _cachedTargetToInstr; FileByteOffset _cachedTextVAddr = 0; }; @@ -82,6 +83,10 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector auto scanner = Codegen::MakeInstructionScanner(); auto instructions = scanner->ScanCodeSection(textSection, 0, textSection.size()); _cachedTargetToInstr.clear(); + const auto referenceCount = std::count_if(instructions.begin(), instructions.end(), [&](const auto& match) { + return isRipRelativeFF(textSection, match) || isRipRelativeMov64(textSection, match); + }); + _cachedTargetToInstr.reserve(static_cast(referenceCount)); for (const auto& m : instructions) { std::size_t dispOff = 0; if (isRipRelativeFF(textSection, m)) @@ -94,15 +99,20 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector std::int32_t disp = readDisp32(textSection, dispOff); VirtualAddress instrEnd = textSectionVAddr + m.Offset + static_cast(m.Length); VirtualAddress target = static_cast(static_cast(instrEnd) + disp); - _cachedTargetToInstr.emplace(target, m.Offset); + _cachedTargetToInstr.emplace_back(target, m.Offset); } + std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); _cachedTextPtr = &textSection; _cachedTextVAddr = textSectionVAddr; } std::vector sites; - auto lower = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress); - auto upper = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress + targetGotOrPltSize); + const auto findTarget = [&](VirtualAddress target) { + return std::lower_bound(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end(), + std::pair{target, 0}); + }; + auto lower = findTarget(targetGotOrPltAddress); + auto upper = findTarget(targetGotOrPltAddress + targetGotOrPltSize); for (auto it = lower; it != upper; ++it) sites.push_back(textSectionVAddr + it->second); return sites; diff --git a/core/relinker/relinker/tests/CallSiteIndexTests.cpp b/core/relinker/relinker/tests/CallSiteIndexTests.cpp new file mode 100644 index 000000000..fceff1c26 --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteIndexTests.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Incorrect call-site index"); } + +int main() { + try { + std::vector code; + const std::vector targets{0x2000, 0x1000, 0x2000, 0x3000, 0x1008}; + for (const auto target : targets) { + const auto offset = code.size(); + code.insert(code.end(), {0xFF, 0x15, 0, 0, 0, 0}); + const auto disp = static_cast(target - (0x100 + offset + 6)); + std::memcpy(code.data() + offset + 2, &disp, 4); + } + auto resolver = Relinker::MakeCallSiteResolver(); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 8) == std::vector{0x106}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 9) == std::vector{0x106, 0x118}); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 0).empty()); + Require(resolver->ResolveCallSites(code, 0x100, 0x4000, 8).empty()); + auto copy = code; + Require(resolver->ResolveCallSites(copy, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(copy, 0x200, 0x2100, 8) == std::vector{0x200, 0x20C}); + std::vector mov{0x48, 0x8B, 0x05, 0xF9, 0x0F, 0, 0}; + Require(resolver->ResolveCallSites(mov, 0x1000, 0x2000, 8) == std::vector{0x1000}); + Require(resolver->ResolveCallSites({}, 0, 0, 1).empty()); + bool rejected = false; + try { resolver->ResolveCallSites(code, 0, std::numeric_limits::max(), 1); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + std::cout << "10 call-site index cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} From 5731e02841bd763c79e6e5d180249a4ea233644d Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 12 Oct 2026 00:02:17 +0200 Subject: [PATCH 119/318] fix(libkernel): close guest sockets in sceKernelClose sceKernelClose passed every descriptor to the host close. Guest socket descriptors start at 0x10000000 and are not host descriptors, so closing an open socket returned SCE_KERNEL_ERROR_EBADF and left it in the guest socket table with its host socket open. Route them to GuestSockets::Close as close, sceKernelRead and sceKernelWrite already do; a closed socket still returns SCE_KERNEL_ERROR_EBADF. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Open.cpp | 1 + core/libs/tests/GuestSocketReadWrite.cpp | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index bfcf004c5..169591644 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -224,6 +224,7 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } int APS5_VABI sceKernelClose(int d) { + if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); File::ForgetRandomDevice(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); diff --git a/core/libs/tests/GuestSocketReadWrite.cpp b/core/libs/tests/GuestSocketReadWrite.cpp index b89370b8c..5a8d48781 100644 --- a/core/libs/tests/GuestSocketReadWrite.cpp +++ b/core/libs/tests/GuestSocketReadWrite.cpp @@ -30,6 +30,7 @@ int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); std::int64_t APS5_VABI _read_nid_postfix(int, void*, std::size_t); @@ -167,6 +168,8 @@ int main() { Require(pwrite_nid_disambig1_nid_postfix(server, request, 1, 0) == -1 && *__error_nid_postfix() == 9); Require(preadv_nid_postfix(server, positionedIov, 1, 0) == -1 && *__error_nid_postfix() == 9); Require(pwritev_nid_postfix(server, positionedIov, 1, 0) == -1 && *__error_nid_postfix() == 9); - Require(close_nid_postfix(listener) == 0); + Require(sceKernelClose(listener) == 0); + Require(sceKernelClose(listener) == KernelBadDescriptor); + Require(close_nid_postfix(listener) == -1 && *__error_nid_postfix() == 9); return 0; } From c63294027bc6067368616c5011c423f705f846b0 Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:06:44 -0400 Subject: [PATCH 120/318] fix(libc): exp2 leaves errno unchanged exp2 set errno (34) for 1024.0. FreeBSD msun never sets errno and reports errors through floating-point exceptions only, so errno is now saved and restored around the host call. Co-Authored-By: Claude Haiku 5.5 --- core/libs/prx/libc/src/Math.cpp | 7 ++++++- core/libs/tests/GuestMath.cpp | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index f6fcf1608..8f02db0fb 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -59,7 +59,12 @@ double APS5_VABI hypot_nid_postfix(double x, double y) { float APS5_VABI tanf_nid_postfix(float x) { return std::tan(x); } float APS5_VABI log10f_nid_postfix(float x) { return std::log10(x); } float APS5_VABI logbf_nid_postfix(float x) { return std::logb(x); } -double APS5_VABI exp2_nid_postfix(double x) { return std::exp2(x); } +double APS5_VABI exp2_nid_postfix(double x) { + int saved_errno = errno; + double result = std::exp2(x); + errno = saved_errno; + return result; +} double APS5_VABI ldexp_nid_postfix(double x, int exponent) { return std::ldexp(x, exponent); } double APS5_VABI scalbn_nid_postfix(double x, int exponent) { return std::scalbn(x, exponent); } float APS5_VABI scalbnf_nid_postfix(float x, int exponent) { return std::scalbn(x, exponent); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 409a96b94..c45aec87c 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -136,6 +136,15 @@ static void CheckAsinfErrno() { Require(errno == 0); } +static void CheckExp2Errno() { + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + double result = exp2_nid_postfix(1024.0); + Require(errno == 0); + Require(std::isinf(result) && result > 0); + Require(std::fetestexcept(FE_OVERFLOW | FE_INEXACT) == (FE_OVERFLOW | FE_INEXACT)); +} + static void CheckLongDouble() { Require(ExtendedBits(acosl_nid_postfix(-1.0L), 0xc90fdaa22168c235ull, 0x4000)); Require(ExtendedBits(acosl_nid_postfix(0.0L), 0xc90fdaa22168c235ull, 0x3fff)); @@ -463,6 +472,7 @@ int main() { Require(exp2_nid_postfix(-3.) == 0.125); Require(ldexp_nid_postfix(0.75, 4) == 12.); CheckSquareRoot(); + CheckExp2Errno(); CheckExactOperations(); CheckLongDouble(); Require(scalbn_nid_postfix(0.75, -2) == 0.1875); From 2b8f151b3f3d9e887ff1176440b1905dc3199c3a Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:09:51 -0400 Subject: [PATCH 121/318] test(agc): verify optimized pipeline draws and readback --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 18 +- .../Execution/include/VulkanDevice.hpp | 1 + .../execution/PipelineOptimizedReadback.cpp | 276 ++++++++++++++++++ 3 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e77f027d1..20c42fc92 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1038,7 +1038,7 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vector_window_tests agc_constant_bit_test_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests}) - if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests") + if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") @@ -1067,6 +1067,22 @@ if(BUILD_TESTING) set_tests_properties(agc_shared_write_partial_collect agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_target_properties(agc_driver_pipeline_optimized_readback_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(pipelineReadbackCase IN ITEMS d32 d16 color32 color64) + foreach(pipelineReadbackMode IN ITEMS default fast) + set(pipelineReadbackTest agc_driver_pipeline_optimized_readback_${pipelineReadbackCase}) + set(pipelineReadbackEnvironment "ANYPS5_NO_SHADER_CACHE=1") + if(pipelineReadbackMode STREQUAL "fast") + string(APPEND pipelineReadbackTest "_fast") + list(APPEND pipelineReadbackEnvironment "APS5_NO_PIPELINE_LTO=1") + endif() + add_test(NAME ${pipelineReadbackTest} COMMAND agc_driver_pipeline_optimized_readback_tests ${pipelineReadbackCase}) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT "${pipelineReadbackEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 433582775..fe9ea5117 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -302,6 +302,7 @@ public: private: VulkanDevice(const PresentationWindow* window, bool compileOnly); + friend struct VulkanDeviceTestAccess; // The device's Graphics::Context: a copy of the one built at setup (its instance functions // resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew. Graphics::Context graphicsContext() const; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp new file mode 100644 index 000000000..e4e66f07f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -0,0 +1,276 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; +alignas(256) constexpr std::array MaskedPixelCode{ + 0x7e040f00, 0x36060481, 0x7da40680, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +alignas(256) constexpr std::array FullPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; +constexpr std::array, 12> DepthTriangles{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 1.0f, 0.25f, 1.0f}, + {-1.0f, 1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {0.0f, 1.0f, 0.25f, 1.0f}, + {0.0f, -1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.0f, 1.0f, 0.75f, 1.0f}, + {0.0f, 1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {1.0f, 1.0f, 0.75f, 1.0f} +}}; + +struct PipelineTrace { + PFN_vkGetDeviceProcAddr resolve = nullptr; + PFN_vkCreateGraphicsPipelines create = nullptr; + PFN_vkCmdBindPipeline bind = nullptr; + PFN_vkCmdDraw draw = nullptr; + DeviceFunctions functions{}; + std::mutex mutex; + std::condition_variable released; + bool allowOptimization = false; + std::vector fast; + std::vector optimized; + VkCommandBuffer boundCommands = VK_NULL_HANDLE; + VkPipeline bound = VK_NULL_HANDLE; + std::vector drawn; + + void ReleaseOptimization() { + { + std::lock_guard lock(mutex); + allowOptimization = true; + } + released.notify_all(); + } +}; + +PipelineTrace* Trace = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + const bool optimizing = std::any_of(infos, infos + count, [](const auto& info) { return (info.flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0; }); + if (optimizing) { + std::unique_lock lock(Trace->mutex); + Trace->released.wait(lock, [] { return Trace->allowOptimization; }); + } + const auto result = Trace->create(device, cache, count, infos, allocator, pipelines); + std::lock_guard lock(Trace->mutex); + for (std::uint32_t index = 0; index < count; ++index) { + if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue; + auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast; + handles.push_back(pipelines[index]); + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL BindPipeline(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) { + if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) { + Trace->boundCommands = commands; + Trace->bound = pipeline; + } + Trace->bind(commands, point, pipeline); +} + +VKAPI_ATTR void VKAPI_CALL DrawCommands(VkCommandBuffer commands, std::uint32_t vertices, std::uint32_t instances, std::uint32_t firstVertex, std::uint32_t firstInstance) { + Trace->drawn.push_back(Trace->boundCommands == commands ? Trace->bound : VK_NULL_HANDLE); + Trace->draw(commands, vertices, instances, firstVertex, firstInstance); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast(CreatePipelines); + if (std::strcmp(name, "vkCmdBindPipeline") == 0) return reinterpret_cast(BindPipeline); + if (std::strcmp(name, "vkCmdDraw") == 0) return reinterpret_cast(DrawCommands); + return Trace->resolve(device, name); +} + +struct OptimizationDrain { + VkDevice device; + ~OptimizationDrain() { + Trace->ReleaseOptimization(); + WaitForOptimizedPipelines(device); + } +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void CheckPixels(bool masked, std::string_view phase) { + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const bool drawn = masked ? (pixel % Width) % 2u == 0u : pixel % Width < Width / 2u; + const auto expected = drawn ? std::byte{255} : Kept; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = Pixels[pixel * 4u + channel]; + Require(actual == expected, std::string(phase) + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected " + std::to_string(static_cast(expected))); + } + } +} + +void CheckDrawn(PipelineTrace& trace, bool optimized) { + std::lock_guard lock(trace.mutex); + const auto& expected = optimized ? trace.optimized : trace.fast; + Require(trace.drawn.size() == 1, "pipeline readback did not observe exactly one real draw"); + Require(std::find(expected.begin(), expected.end(), trace.drawn.front()) != expected.end(), optimized ? "readback draw did not bind an optimized pipeline" : "readback draw did not bind a fast-linked pipeline"); +} + +void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace, std::uint32_t waveSize, VkFormat depthFormat, bool optimize) { + const bool masked = depthFormat == VK_FORMAT_UNDEFINED; + const auto target = device.Target(); + const auto vertices = masked ? std::span>(Triangle) : std::span>(DepthTriangles); + const auto vertexBuffer = BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + const std::vector vertexUserData(vertexBuffer.begin(), vertexBuffer.end()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + const auto pixelCode = masked ? std::span(MaskedPixelCode) : std::span(FullPixelCode); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + if (!masked) { + state.depth = DepthTarget{reinterpret_cast(DepthMemory.data()), 0, {Width, Height}, depthFormat, 0.5f, 0}; + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + } + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + const auto render = [&](bool optimized, std::string_view phase) { + Pixels.fill(Kept); + trace.drawn.clear(); + trace.bound = VK_NULL_HANDLE; + trace.boundCommands = VK_NULL_HANDLE; + AgcDriver::Graphics::Draw(context, state, draw, shaders); + device.WaitIdle(); + CheckDrawn(trace, optimized); + CheckPixels(masked, phase); + }; + render(false, "fast link"); + trace.ReleaseOptimization(); + WaitForOptimizedPipelines(context.device); + Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries"); + Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode"); + render(optimize, "completed link"); + std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size()); +} + +} + +int main(int argc, char** argv) { + try { + std::string_view mode = "d32"; + bool force = false; + for (int index = 1; index < argc; ++index) { + const std::string_view argument = argv[index]; + if (argument == "--force-lto") force = true; + else { + Require(argument == "color32" || argument == "color64" || argument == "d32" || argument == "d16", "unknown pipeline readback case"); + mode = argument; + } + } + PipelineTrace trace; + Trace = &trace; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.graphicsPipelineLibrary) { + std::puts("skipped, graphics pipeline libraries are unavailable or disabled"); + return VulkanTestSkipped; + } + const bool masked = mode == "color32" || mode == "color64"; + if (masked && device->Target().subgroupSize < 32u) { + std::puts("skipped, the masked pixel shader requires at least 32 subgroup lanes"); + return VulkanTestSkipped; + } + if (force) context.pipelineLinkTimeOptimization = true; + const bool optimize = context.pipelineLinkTimeOptimization && std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; + std::printf("pipeline readback: %s, %.*s, LTO %s%s\n", device->DeviceName().c_str(), static_cast(mode.size()), mode.data(), optimize ? "enabled" : "disabled", force ? " (explicit test override)" : " (production policy)"); + trace.resolve = context.deviceProc; + trace.create = context.Function("vkCreateGraphicsPipelines"); + trace.bind = context.Function("vkCmdBindPipeline"); + trace.draw = context.Function("vkCmdDraw"); + trace.functions = context.functions != nullptr ? *context.functions : DeviceFunctions{}; + trace.functions.cmdBindPipeline = BindPipeline; + trace.functions.cmdDraw = DrawCommands; + context.functions = &trace.functions; + context.deviceProc = Resolve; + OptimizationDrain drain{context.device}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Run(*device, context, trace, mode == "color32" ? 32u : 64u, masked ? VK_FORMAT_UNDEFINED : (mode == "d16" ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT), optimize); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 972caa4f2a3079f7f60a76f508880053d957bade Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 10 Oct 2026 12:45:09 -0700 Subject: [PATCH 122/318] fix(libkernel): number pthread keys from 1 as FreeBSD libthr does scePthreadKeyCreate and pthread_key_create handed out the slot index, so the first key in a process was 0. FreeBSD libthr returns slot + 1 (_pthread_key_create: "*key = i + 1") and subtracts 1 again in pthread_key_delete, pthread_getspecific and pthread_setspecific, so 0 is never a valid key. Code that keeps 0 as "key not created yet" therefore created a new key on every call and lost the values it stored under the previous one. Keys now start at 1; 0 and other out-of-range keys are rejected with EINVAL by delete and set and read as NULL, as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XYhZu5HokHMo9mfi8QU83m --- core/libs/prx/libkernel/Pthread/src/Tsd.cpp | 30 +++++++++++++-------- core/libs/tests/GuestPthreadKey.cpp | 10 +++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp index f9fa4ecc6..03c706c20 100644 --- a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp @@ -64,6 +64,11 @@ struct ThreadValues { } }; +static int KeySlot(PthreadKey key) { + const unsigned slot = static_cast(key) - 1u; + return slot < static_cast(MAX_KEYS) ? static_cast(slot) : -1; +} + static ThreadValues& Values() { #ifdef _WIN32 return WindowsThreadLocal::Get(); @@ -86,7 +91,7 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t if (g_keys[index].sequence.load(std::memory_order_relaxed) == FREE_KEY) { g_keys[index].destructor = reinterpret_cast(destructor); g_keys[index].sequence.store(++g_lastSequence, std::memory_order_release); - *key = index; + *key = index + 1; return SCE_OK; } } @@ -94,25 +99,28 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t } int APS5_VABI scePthreadKeyDelete(PthreadKey key) { - if (key < 0 || key >= MAX_KEYS) return SCE_KERNEL_ERROR_EINVAL; + const int slot = KeySlot(key); + if (slot < 0) return SCE_KERNEL_ERROR_EINVAL; std::lock_guard lock(g_keyLock); - if (g_keys[key].sequence.load(std::memory_order_relaxed) == FREE_KEY) return SCE_KERNEL_ERROR_EINVAL; - g_keys[key].sequence.store(FREE_KEY, std::memory_order_release); - g_keys[key].destructor = nullptr; + if (g_keys[slot].sequence.load(std::memory_order_relaxed) == FREE_KEY) return SCE_KERNEL_ERROR_EINVAL; + g_keys[slot].sequence.store(FREE_KEY, std::memory_order_release); + g_keys[slot].destructor = nullptr; return SCE_OK; } void* APS5_VABI scePthreadGetspecific(PthreadKey key) { - if (key < 0 || key >= MAX_KEYS) return nullptr; - const ThreadValue& entry = Values().values[key]; - return entry.sequence == g_keys[key].sequence.load(std::memory_order_acquire) ? entry.value : nullptr; + const int slot = KeySlot(key); + if (slot < 0) return nullptr; + const ThreadValue& entry = Values().values[slot]; + return entry.sequence == g_keys[slot].sequence.load(std::memory_order_acquire) ? entry.value : nullptr; } int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value) { - if (key < 0 || key >= MAX_KEYS) return SCE_KERNEL_ERROR_EINVAL; - const std::uint64_t sequence = g_keys[key].sequence.load(std::memory_order_acquire); + const int slot = KeySlot(key); + if (slot < 0) return SCE_KERNEL_ERROR_EINVAL; + const std::uint64_t sequence = g_keys[slot].sequence.load(std::memory_order_acquire); if (sequence == FREE_KEY) return SCE_KERNEL_ERROR_EINVAL; - Values().values[key] = {value, sequence}; + Values().values[slot] = {value, sequence}; return SCE_OK; } diff --git a/core/libs/tests/GuestPthreadKey.cpp b/core/libs/tests/GuestPthreadKey.cpp index e9bdd1653..ae87651a6 100644 --- a/core/libs/tests/GuestPthreadKey.cpp +++ b/core/libs/tests/GuestPthreadKey.cpp @@ -10,6 +10,8 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t int APS5_VABI scePthreadKeyDelete(PthreadKey key); void* APS5_VABI scePthreadGetspecific(PthreadKey key); int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); +int APS5_VABI pthread_key_create_nid_postfix(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI pthread_key_delete_nid_postfix(PthreadKey key); } static constexpr int SCE_OK = 0; @@ -62,8 +64,16 @@ static void* APS5_VABI HoldAcrossReuse(void*) { } int main() { + PthreadKey posixKey = 0; + Require(pthread_key_create_nid_postfix(&posixKey, nullptr) == 0 && posixKey != 0); + Require(scePthreadSetspecific(0, &first) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadGetspecific(0) == nullptr); + Require(scePthreadKeyDelete(0) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_key_delete_nid_postfix(posixKey) == 0); + PthreadKey key = -1; Require(scePthreadKeyCreate(&key, nullptr) == SCE_OK); + Require(key != 0); Require(scePthreadGetspecific(key) == nullptr); Require(scePthreadSetspecific(key, &first) == SCE_OK); Require(scePthreadGetspecific(key) == &first); From 931aaa2a7e49b75d8ee28f2ba0625f4b36738dd5 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:09:22 +0530 Subject: [PATCH 123/318] feat(libkernel): handle sysctl queries for pathname, physmem and cacheline size --- .../System/src/SystemConfiguration.cpp | 70 +++++++++++++---- core/libs/tests/GuestSystemConfiguration.cpp | 77 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 2 + 3 files changed, 134 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index abbd8d0ea..106e28393 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -16,6 +16,7 @@ #endif extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI getpid_nid_postfix(); struct SceKernelSystemSwVersion { std::uint64_t size; @@ -90,19 +91,46 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } - if (nameLength != 2 || name[0] != 6 || name[1] != 3) { - std::string mib; - for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); - throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); + if (nameLength == 2 && name[0] == 6 && name[1] == 3) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::int64_t processors = sysconf_nid_postfix(58); + if (processors <= 0) return -1; + const int value = static_cast(processors); + return CopyOut(&value, sizeof(value), oldValue, oldLength); } - if (newValue != nullptr) { - *__error_nid_postfix() = 1; - return -1; + if (nameLength == 2 && name[0] == 6 && name[1] == 5) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::int64_t pages = sysconf_nid_postfix(121); + if (pages <= 0) return -1; + const std::uint64_t physmem = static_cast(pages) * PS5_PAGE_SIZE; + return CopyOut(&physmem, sizeof(physmem), oldValue, oldLength); } - const std::int64_t processors = sysconf_nid_postfix(58); - if (processors <= 0) return -1; - const int value = static_cast(processors); - return CopyOut(&value, sizeof(value), oldValue, oldLength); + if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 12) { + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const int pid = name[3]; + if (pid != -1 && pid != getpid_nid_postfix()) { + *__error_nid_postfix() = 3; + return -1; + } + static constexpr char path[] = "/app0/eboot.bin"; + return CopyOut(path, sizeof(path), oldValue, oldLength); + } + std::string mib; + for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); + throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); } int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::size_t* oldLength, const void* newValue, std::size_t newLength) { @@ -110,6 +138,14 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 14; return -1; } + if (std::strcmp(name, "machdep.cacheline_size") == 0) { + *__error_nid_postfix() = 2; + return -1; + } + if (std::strcmp(name, "kern.proc.pathname") == 0) { + *__error_nid_postfix() = 22; + return -1; + } if (std::strcmp(name, "kern.rng_pseudo") == 0) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -121,9 +157,15 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); return 0; } - if (std::strcmp(name, "hw.ncpu") != 0) throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); - static constexpr int mib[] = {6, 3}; - return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + if (std::strcmp(name, "hw.ncpu") == 0) { + static constexpr int mib[] = {6, 3}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + if (std::strcmp(name, "hw.physmem") == 0) { + static constexpr int mib[] = {6, 5}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); } int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* version) { diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index ffba40d37..e29adaa60 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -21,6 +21,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sysctl_nid_postfix(const int*, std::uint32_t, void*, std::size_t*, const void*, std::size_t); int APS5_VABI sysctlbyname_nid_postfix(const char*, void*, std::size_t*, const void*, std::size_t); int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion*); +int APS5_VABI getpid_nid_postfix(); extern char** environ_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -56,7 +57,7 @@ static void CheckProcessorCountSysctl() { } Require(threw); threw = false; - const int unknown[] = {6, 5}; + const int unknown[] = {6, 99}; try { sysctl_nid_postfix(unknown, 2, &value, &length, nullptr, 0); } catch (const std::runtime_error&) { @@ -64,6 +65,77 @@ static void CheckProcessorCountSysctl() { } Require(threw); } +static void CheckPhysicalMemorySysctl() { + const int mib[] = {6, 5}; + const std::uint64_t expected = static_cast(sysconf_nid_postfix(121)) * 0x4000; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint64_t)); + std::uint64_t value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + value = 0; + length = 16; + Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[8] = {0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}; + length = 4; + Require(sysctlbyname_nid_postfix("hw.physmem", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 4 && bytes[4] == 0xaa); + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); +} +static void CheckMachdepCachelineSizeSysctl() { + *__error_nid_postfix() = 13; + int value = 0x5a5a5a5a; + std::size_t length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.cacheline_size", &value, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + Require(value == 0x5a5a5a5a); +} +static void CheckPathnameSysctl() { + *__error_nid_postfix() = 13; + char buffer[32] = {}; + std::size_t length = sizeof(buffer); + Require(sysctlbyname_nid_postfix("kern.proc.pathname", buffer, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 22); + + const int mibTooShort[] = {1, 14, 12}; + Require(sysctl_nid_postfix(mibTooShort, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + const int mibTooLong[] = {1, 14, 12, -1, 0}; + Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + const int mibSelf[] = {1, 14, 12, -1}; + length = 0; + Require(sysctl_nid_postfix(mibSelf, 4, nullptr, &length, nullptr, 0) == 0 && length == sizeof("/app0/eboot.bin")); + + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int callerPid = getpid_nid_postfix(); + const int mibPid[] = {1, 14, 12, callerPid}; + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibPid, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int mibForeign[] = {1, 14, 12, callerPid + 1000}; + Require(sysctl_nid_postfix(mibForeign, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + + length = 5; + std::memset(buffer, 0xaa, sizeof(buffer)); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 5 && buffer[4] == '0' && buffer[5] == static_cast(0xaa)); + + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); @@ -109,6 +181,9 @@ static void CheckPseudoRandomSysctl() { } int main() { CheckProcessorCountSysctl(); + CheckPhysicalMemorySysctl(); + CheckMachdepCachelineSizeSysctl(); + CheckPathnameSysctl(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); *__error_nid_postfix() = 13; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a907d34b4..1522f23f7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -444,6 +444,8 @@ Throughout the project, every function at every stage either **does exactly what - [Packed horizontal gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. - [waitpid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. +- [kern.proc.pathname](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. +- [machdep.cacheline_size](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. - [execvp](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [MIMG BY2/BY4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. From 1d5f5eb6c8cac0e5536468dd0e64d44f1774af43 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 09:52:56 +0200 Subject: [PATCH 124/318] feat(libSceJson2): implement the InitParameter2 constructors and setSpecialFloatFormatType Resident Evil 4 (PPSA07411, #2776) and PRAGMATA (PPSA02530, #3043) stop on the absent export with what(): _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE not implemented Deathloop, Minecraft Legends and Cyberpunk import the base-object constructor InitParameter2::InitParameter2(), InitParameterRtti2:: setAllocatorRtti and InitParameterRtti::InitParameterRtti(AllocParamRtti*, void*, unsigned long), which were NotImplemented stubs. InitParameter2 is the 40-byte object of SharpEmu and KytyPS5: the allocator, its user data, the file buffer size and the 32-bit special float format type at 0x18. Both constructors zero it, setAllocatorRtti writes the allocation parameters and user data where setAllocator writes the allocator, and setSpecialFloatFormatType stores the type. InitParameterRtti's constructor writes its three arguments to the first 24 bytes. The NID WVZBP4IyM+E recomputes from the mangled name and is in the aerolib database. The enum's values are unknown, so no value is rejected; the inferred layouts are recorded in TechnicalDebt. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 258f2910808e5ef1636973aaefbdf51615873dcb) --- core/libs/prx/libSceJson2/Export.cpp | 40 +++++++++++------ core/libs/tests/GuestJson.cpp | 67 ++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 19b25beb8..c9085c3cf 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -798,30 +798,53 @@ int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* } -int APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +struct InitParameterRtti { + void* allocParam; + void* userData; + std::size_t fileBufferSize; +}; + +void APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(InitParameterRtti* self, void* allocParam, void* userData, std::size_t fileBufferSize) { + self->allocParam = allocParam; + self->userData = userData; + self->fileBufferSize = fileBufferSize; } + struct InitParameter2 { void* allocator; void* userData; std::size_t fileBufferSize; + std::uint32_t specialFloatFormatType; + std::uint32_t reserved[3]; }; -static_assert(sizeof(InitParameter2) <= 40); +static_assert(sizeof(InitParameter2) == 0x28 && offsetof(InitParameter2, specialFloatFormatType) == 0x18); void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(InitParameter2* self) { *self = {}; } +void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(InitParameter2* self) { + _ZN3sce4Json14InitParameter2C1Ev(self); +} + void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(InitParameter2* self, void* allocator, void* userData) { self->allocator = allocator; self->userData = userData; } +void APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(InitParameter2* self, void* allocParam, void* userData) { + self->allocator = allocParam; + self->userData = userData; +} + void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(InitParameter2* self, std::size_t size) { self->fileBufferSize = size; } +void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(InitParameter2* self, std::uint32_t type) { + self->specialFloatFormatType = type; +} + int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void* self, const InitParameter2* parameter) { (void)self; if (parameter == nullptr) throw std::invalid_argument("sce::Json::Initializer::initialize: null parameter"); @@ -838,15 +861,6 @@ int APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void) { return 0; } -void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(InitParameter2* self) { - _ZN3sce4Json14InitParameter2C1Ev(self); -} - -int APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 6b7309123..1a1c034e9 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -92,6 +92,9 @@ void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(void*); void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(void*, void*, void*); void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(void*, std::size_t); int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void*, const void*); +void APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(void*, void*, void*); +void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(void*, std::uint32_t); +void APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(void*, void*, void*, std::size_t); } static void Check(bool value, int line) { @@ -422,6 +425,69 @@ static void InitParameter2BaseConstructor() { Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); } +struct StoredInitParameter2 { + void* allocator; + void* userData; + std::size_t fileBufferSize; + std::uint32_t specialFloatFormatType; + std::uint32_t reserved[3]; +}; +static_assert(sizeof(StoredInitParameter2) == 40); + +static StoredInitParameter2 Stored(const std::uint8_t* parameter) { + StoredInitParameter2 stored{}; + std::memcpy(&stored, parameter, sizeof(stored)); + return stored; +} + +static bool Untouched(const std::uint8_t* parameter, std::size_t from) { + for (std::size_t i = from; i < 40; ++i) + if (parameter[i] != 0xff) return false; + return true; +} + +static void InitParameters() { + using Constructor = void (APS5_VABI*)(void*); + for (Constructor construct : {_ZN3sce4Json14InitParameter2C1Ev, _ZN3sce4Json14InitParameter2C2Ev}) { + alignas(16) std::uint8_t parameter[40]; + std::memset(parameter, 0xff, sizeof(parameter)); + construct(parameter); + StoredInitParameter2 stored = Stored(parameter); + Require(stored.allocator == nullptr && stored.userData == nullptr && stored.fileBufferSize == 0); + Require(stored.specialFloatFormatType == 0); + Require(stored.reserved[0] == 0 && stored.reserved[1] == 0 && stored.reserved[2] == 0); + + int allocParam = 0; + int userData = 0; + _ZN3sce4Json14InitParameter217setFileBufferSizeEm(parameter, 8192); + _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(parameter, 2); + _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(parameter, &allocParam, &userData); + stored = Stored(parameter); + Require(stored.allocator == &allocParam && stored.userData == &userData && stored.fileBufferSize == 8192); + Require(stored.specialFloatFormatType == 2); + Require(stored.reserved[0] == 0 && stored.reserved[1] == 0 && stored.reserved[2] == 0); + + _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(parameter, 0); + Require(Stored(parameter).specialFloatFormatType == 0 && Stored(parameter).fileBufferSize == 8192); + + alignas(16) std::uint8_t initializer[16]{}; + Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); + Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, parameter) == 0); + Require(_ZN3sce4Json11Initializer9terminateEv(initializer) == 0); + Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); + } + + alignas(16) std::uint8_t rttiParameter[40]; + std::memset(rttiParameter, 0xff, sizeof(rttiParameter)); + int allocParam = 0; + int userData = 0; + _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(rttiParameter, &allocParam, &userData, 4096); + void* rttiStored[3]{}; + std::memcpy(rttiStored, rttiParameter, sizeof(rttiStored)); + Require(rttiStored[0] == &allocParam && rttiStored[1] == &userData && reinterpret_cast(rttiStored[2]) == 4096); + Require(Untouched(rttiParameter, sizeof(rttiStored))); +} + static void ValueClear() { Value value{}; _ZN3sce4Json5ValueC1Ev(&value); @@ -498,6 +564,7 @@ int main() { NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); ValueAccess(); InitParameter2BaseConstructor(); + InitParameters(); ValueClear(); StreamParse(); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a907d34b4..787cec9ac 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -347,7 +347,6 @@ Throughout the project, every function at every stage either **does exactly what - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date -- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes - [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes @@ -645,3 +644,4 @@ Throughout the project, every function at every stage either **does exactly what - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well +- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown From d004d11fa323f695620c9b5cf4357c02c66828d5 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 16:12:12 +0000 Subject: [PATCH 125/318] fix(libSceJson2): use signed special-float enum type --- core/libs/prx/libSceJson2/Export.cpp | 4 ++-- core/libs/tests/GuestJson.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index c9085c3cf..2a7581ba0 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -814,7 +814,7 @@ struct InitParameter2 { void* allocator; void* userData; std::size_t fileBufferSize; - std::uint32_t specialFloatFormatType; + std::int32_t specialFloatFormatType; std::uint32_t reserved[3]; }; static_assert(sizeof(InitParameter2) == 0x28 && offsetof(InitParameter2, specialFloatFormatType) == 0x18); @@ -841,7 +841,7 @@ void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(InitParameter2* self->fileBufferSize = size; } -void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(InitParameter2* self, std::uint32_t type) { +void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(InitParameter2* self, std::int32_t type) { self->specialFloatFormatType = type; } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 1a1c034e9..7450ba5cb 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -93,7 +93,7 @@ void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(void*, std::size_t); int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void*, const void*); void APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(void*, void*, void*); -void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(void*, std::uint32_t); +void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(void*, std::int32_t); void APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(void*, void*, void*, std::size_t); } @@ -429,7 +429,7 @@ struct StoredInitParameter2 { void* allocator; void* userData; std::size_t fileBufferSize; - std::uint32_t specialFloatFormatType; + std::int32_t specialFloatFormatType; std::uint32_t reserved[3]; }; static_assert(sizeof(StoredInitParameter2) == 40); From 8c00fe871e49f01b783e3983b0b38495c2904513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 18:15:13 +0200 Subject: [PATCH 126/318] docs(TechnicalDebt): put the InitParameter2 line where the old one was, not at the end of the file Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 787cec9ac..ca0e50108 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -347,6 +347,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date +- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes - [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes @@ -644,4 +645,3 @@ Throughout the project, every function at every stage either **does exactly what - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well -- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown From 1dc342dd57cff18b0a0681a0d58ae25778918add Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 09:34:06 +1300 Subject: [PATCH 127/318] perf(agc): keep the highest index of a large in-place index buffer while it is unchanged An indexed draw that binds its index buffer in place from a host import (#2253) scans the bound range for its highest index on every draw, where the copied path reuses the value kept with its snapshot. A draw of many indices pays for the scan each time, and an indirect draw, which binds the whole declared index buffer, scans the whole buffer per draw. The recorder now keeps the highest index of an in-place index range of 4096 indices or more with the write generation collected before the scan, and a draw takes it while the range is unchanged since (GuestMemory::UnchangedSince and the allocation registry generation, as for a reused snapshot). A smaller range is scanned at every draw as before: the scan costs 0.4 ns per index and the write collect and lookup 0.2 to 1.4 us per draw, so the scan is the cheaper of the two below about 3,000 indices, as #2253 measured on small ranges. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../libSceAgcDriver/Graphics/include/Draw.hpp | 5 +++ .../Graphics/include/Recorder.hpp | 12 +++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 28 ++++++++++-- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 34 ++++++++++++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 44 +++++++++++++++++++ 6 files changed, 122 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e77f027d1..0338632d4 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -819,8 +819,10 @@ if(BUILD_TESTING) set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) + set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index e59172f7c..d852d6743 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -32,6 +32,11 @@ DrawInputCopy CopyDrawInput(const Context& context, Recorder* recorder, std::uin DrawInputCopy CopyZeroPaddedDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t validBytes); void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCopy& copy, Recorder::SnapshotUse use, std::uint32_t derived); const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment); +struct InPlaceIndices { + std::optional highest; + bool reused = false; +}; +InPlaceIndices InPlaceIndexRange(Recorder& recorder, std::uint64_t address, std::size_t bytes, std::uint32_t indexSize, bool skipRestart); std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 7a863048d..2954c20f7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -99,6 +99,10 @@ public: static constexpr std::size_t DrawInputEntries = 16384; std::shared_ptr ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0); void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0); + static constexpr std::size_t IndexRangeEntries = 16384; + static constexpr std::size_t KeptIndexRangeIndices = 4096; + bool KnownHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::optional& highest); + void KeepHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint64_t generation, std::uint64_t registryGeneration, std::optional highest); void OnComplete(std::function action); void NotePendingWrite(std::uint64_t address, std::size_t bytes); void NotePendingFill(std::uint64_t address, std::size_t bytes, std::uint8_t value); @@ -808,6 +812,14 @@ private: std::map drawSnapshots; std::array drawSnapshotPools; void eraseDrawSnapshot(std::map::iterator entry); + struct IndexRange { + std::uint64_t generation; + std::uint64_t registryGeneration; + std::list::iterator recent; + std::optional highest; + }; + std::map indexRanges; + std::list indexRangeRecency; bool refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes); }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b1fe81c22..09fc3ffbc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -971,6 +971,25 @@ const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address return import; } +InPlaceIndices InPlaceIndexRange(Recorder& recorder, std::uint64_t address, std::size_t bytes, std::uint32_t indexSize, bool skipRestart) { + const std::span range(reinterpret_cast(address), bytes); + InPlaceIndices indices; + if (bytes < Recorder::KeptIndexRangeIndices * indexSize) { + indices.highest = HighestDrawIndex(range, indexSize, skipRestart); + return indices; + } + const auto use = indexSize == 2 ? (skipRestart ? Recorder::SnapshotUse::Index16Restart : Recorder::SnapshotUse::Index16) : (skipRestart ? Recorder::SnapshotUse::Index32Restart : Recorder::SnapshotUse::Index32); + const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + const auto generation = GuestMemory::CollectWrites(address, bytes); + if (generation != 0 && recorder.KnownHighestIndex(address, bytes, use, indices.highest)) { + indices.reused = true; + return indices; + } + indices.highest = HighestDrawIndex(range, indexSize, skipRestart); + recorder.KeepHighestIndex(address, bytes, use, generation, registryGeneration, indices.highest); + return indices; +} + namespace { // The draw's inputs before its resources (prepareDrawInputs): the validated parameters, the index @@ -1077,10 +1096,11 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P if (import == nullptr) copy = CopyDrawInput(context, context.recorder, draw.indexAddress, static_cast(indexBytes), draw.indexSize, use); else GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); std::optional highest; - if (!copy.reused) { - const auto bytes = import != nullptr ? std::span(reinterpret_cast(draw.indexAddress), static_cast(indexBytes)) : copy.buffer->Bytes().first(static_cast(indexBytes)); - highest = HighestDrawIndex(bytes, draw.indexSize, skipRestart); - if (import == nullptr) KeepDrawInput(context.recorder, draw.indexAddress, copy, use, skipRestart ? (highest ? *highest + 1u : 0u) : highest.value_or(0u)); + if (import != nullptr) { + highest = InPlaceIndexRange(*context.recorder, draw.indexAddress, static_cast(indexBytes), draw.indexSize, skipRestart).highest; + } else if (!copy.reused) { + highest = HighestDrawIndex(copy.buffer->Bytes().first(static_cast(indexBytes)), draw.indexSize, skipRestart); + KeepDrawInput(context.recorder, draw.indexAddress, copy, use, skipRestart ? (highest ? *highest + 1u : 0u) : highest.value_or(0u)); } else if (!skipRestart) { highest = copy.derived; } else if (copy.derived != 0) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index c788418b9..4c660d44f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2561,6 +2561,40 @@ void Recorder::KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::u pool.bytes += bytes; } +bool Recorder::KnownHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::optional& highest) { + const auto found = indexRanges.find({address, use, bytes}); + if (found == indexRanges.end()) return false; + if (found->second.registryGeneration != GuestAllocations::GuestAllocationsGeneration_nid_postfix() || !GuestMemory::UnchangedSince(address, bytes, found->second.generation)) { + indexRangeRecency.erase(found->second.recent); + indexRanges.erase(found); + return false; + } + indexRangeRecency.splice(indexRangeRecency.end(), indexRangeRecency, found->second.recent); + highest = found->second.highest; + return true; +} + +void Recorder::KeepHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint64_t generation, std::uint64_t registryGeneration, std::optional highest) { + Require(use != SnapshotUse::Storage && use != SnapshotUse::Vertex, "a highest index belongs to an index buffer"); + if (generation == 0) return; + const DrawSnapshotKey key{address, use, bytes}; + if (const auto found = indexRanges.find(key); found != indexRanges.end()) { + indexRangeRecency.erase(found->second.recent); + indexRanges.erase(found); + } + while (indexRanges.size() >= IndexRangeEntries) { + indexRanges.erase(indexRangeRecency.front()); + indexRangeRecency.pop_front(); + } + indexRangeRecency.push_back(key); + try { + indexRanges.emplace(key, IndexRange{generation, registryGeneration, std::prev(indexRangeRecency.end()), highest}); + } catch (...) { + indexRangeRecency.pop_back(); + throw; + } +} + void Recorder::OnComplete(std::function action) { ensureOpen(); open->completions.push_back(std::move(action)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 5484435e9..99e4f9a77 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2081,6 +2081,45 @@ void drawInputInPlaceTests(const Device& device, Recorder& recorder) { Require(InPlaceDrawInput(unrecorded, vertices, 256, 4) == nullptr, "a draw without a recorder bound its input in place"); recorder.Sync(); Require(!recorder.PendingReadOverlaps(vertices, 256), "an in-place draw input's read outlived its batch"); + if (!AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "host imports are compared, not watched: kept highest indices not tested\n"; + return; + } + const auto indices = address + 20480; + constexpr std::size_t kept = Recorder::KeptIndexRangeIndices * 2; + std::vector first(Recorder::KeptIndexRangeIndices * 2, 1); + first[0] = 3; + first[1] = 9; + first[2] = 0xffff; + std::memcpy(reinterpret_cast(indices), first.data(), kept * 2); + auto range = InPlaceIndexRange(recorder, indices, kept, 2, false); + Require(!range.reused && range.highest == 0xffffu, "an in-place index range was not scanned at its first use"); + range = InPlaceIndexRange(recorder, indices, kept, 2, false); + Require(range.reused && range.highest == 0xffffu, "an unchanged in-place index range was scanned again"); + range = InPlaceIndexRange(recorder, indices, kept, 2, true); + Require(!range.reused && range.highest == 9u, "an in-place index range served a scan made under the other restart rule"); + Require(!InPlaceIndexRange(recorder, indices, kept + 2, 2, true).reused, "an in-place index range served a scan of another size"); + const std::uint16_t raised = 12; + std::memcpy(reinterpret_cast(indices + 2), &raised, sizeof(raised)); + range = InPlaceIndexRange(recorder, indices, kept, 2, true); + Require(!range.reused && range.highest == 12u, "an in-place index range kept its highest index over a guest write"); + range = InPlaceIndexRange(recorder, indices, kept, 2, true); + Require(range.reused && range.highest == 12u, "a rescanned in-place index range was not kept"); + range = InPlaceIndexRange(recorder, indices, kept - 2, 2, true); + Require(!range.reused && range.highest == 12u, "an in-place index range below the kept size was not scanned"); + range = InPlaceIndexRange(recorder, indices, kept - 2, 2, true); + Require(!range.reused && range.highest == 12u, "an in-place index range below the kept size was kept"); + range = InPlaceIndexRange(recorder, indices, kept * 2 - 4, 4, false); + Require(!range.reused && range.highest == 0x000c0003u, "a 32-bit in-place index range below the kept size was not scanned"); + Require(!InPlaceIndexRange(recorder, indices, kept * 2 - 4, 4, false).reused, "a 32-bit in-place index range below the kept size was kept"); + Require(!InPlaceIndexRange(recorder, indices, kept * 2, 4, false).reused, "a 32-bit in-place index range was not scanned at its first use"); + range = InPlaceIndexRange(recorder, indices, kept * 2, 4, false); + Require(range.reused && range.highest == 0x000c0003u, "an unchanged 32-bit in-place index range was scanned again"); + const std::vector restarts(Recorder::KeptIndexRangeIndices, 0xffff); + std::memcpy(reinterpret_cast(indices + 20480), restarts.data(), kept); + Require(!InPlaceIndexRange(recorder, indices + 20480, kept, 2, true).highest.has_value(), "an index range of restart indices has a highest index"); + range = InPlaceIndexRange(recorder, indices + 20480, kept, 2, true); + Require(range.reused && !range.highest.has_value(), "an empty in-place index range was scanned again"); } // Unit shadows (UnitShadow.hpp) over a host import of write-watched arena memory: a retile piece's @@ -5374,6 +5413,11 @@ int main(int argc, char** argv) { std::cout << "CMASK copied clear, write-back and failure tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--draw-inputs-in-place-only") { + drawInputInPlaceTests(device, recorder); + std::cout << "In-place draw input tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") { multisampleGuestAccessTests(device, recorder); return 0; From feb19a844d14fe77c68c1c71c95ce8508c2dc053 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 22:21:32 +0000 Subject: [PATCH 128/318] fix(relinker): decode 3DNow! instructions with their ModRM and opcode suffix 0F 0F was decoded as a two-byte instruction. 3DNow! instructions are 0F 0F ModRM [SIB] [displacement] imm8, the trailing byte selecting the operation (AMD64 Architecture Programmer's Manual Volume 3, Appendix A, 3DNow! opcodes). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VZWLdTjf1S57iLgn2rcaFb --- .../codegen/include/codegen/x86/X64OpcodeConstants.hpp | 1 + core/relinker/codegen/src/x86/X64InstructionDecoder.cpp | 2 +- core/relinker/codegen/tests/DecoderLengthCases.hpp | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 889d0c447..7a9a14a36 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -170,6 +170,7 @@ inline constexpr std::uint8_t TwoByteGrp6 = 0x00; inline constexpr std::uint8_t TwoByteLar = 0x02; inline constexpr std::uint8_t TwoByteLsl = 0x03; inline constexpr std::uint8_t TwoBytePrefetchw = 0x0D; +inline constexpr std::uint8_t TwoByte3DNow = 0x0F; inline constexpr std::uint8_t TwoByteMovCrDrMin = 0x20; inline constexpr std::uint8_t TwoByteMovCrDrMax = 0x23; inline constexpr std::uint8_t TwoBytePopcnt = 0xB8; diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index b894332bd..ab5785a01 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -242,7 +242,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t hasModRm = true; } else if (opcode >= TwoByteJccRel32Min && opcode <= TwoByteJccRel32Max) { immediateSize = ImmSize32; - } else if (opcode == TwoByteMovImm8ModRm) { + } else if (opcode == TwoByteMovImm8ModRm || opcode == TwoByte3DNow) { hasModRm = true; immediateSize = ImmSize8; } else if (opcode >= TwoByteShiftImm8Min && opcode <= TwoByteShiftImm8Max) { diff --git a/core/relinker/codegen/tests/DecoderLengthCases.hpp b/core/relinker/codegen/tests/DecoderLengthCases.hpp index 561f3a64e..10747c9a6 100644 --- a/core/relinker/codegen/tests/DecoderLengthCases.hpp +++ b/core/relinker/codegen/tests/DecoderLengthCases.hpp @@ -33,6 +33,12 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"0f0b", 2}, {"0f0d846511223344", 8}, {"0f0e", 2}, + {"0f0fc196", 4}, + {"0f0f08b7", 4}, + {"0f0f4f80b4", 5}, + {"0f0f846511223344b4", 9}, + {"0f0f0d11223344a6", 8}, + {"410f0f1c24bf", 6}, {"0f10846511223344", 8}, {"0f11846511223344", 8}, {"0f12846511223344", 8}, From 43573fd48a98eb282520fa32f51a1921a8f45e5f Mon Sep 17 00:00:00 2001 From: Simon <340958984+ee6aba54@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:17:23 +0200 Subject: [PATCH 129/318] fix(libSceVideoOut): return INVALID_HANDLE for events on ports that are not open Registering a flip or vblank event on a handle that sceVideoOutOpen never created, or that was closed, threw, which ended titles that register their flip event before they open the port and ignore the result. The documented error SCE_VIDEO_OUT_ERROR_INVALID_HANDLE is returned instead, the same check sceVideoOutSetFlipRate and the other output calls use. The flip test program gains an "unopened" mode. --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 2 +- core/libs/prx/libSceVideoOut/src/Event.cpp | 1 + core/libs/prx/libSceVideoOut/tests/Flip.cpp | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 7485b2123..d99e91855 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -37,7 +37,7 @@ if(BUILD_TESTING) "SDL video driver for VideoOut flip tests") set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flipMode IN ITEMS lifetime reopen decode controls overlay) + foreach(flipMode IN ITEMS lifetime reopen decode controls overlay unopened) if(flipMode STREQUAL "lifetime") add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests) else() diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index a4853cd0a..a2a425f24 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -18,6 +18,7 @@ static std::vector* getEventList(VideoOutConfig& cfg, int16_t } static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind, void* udata) { + if (!VideoOutDriver::Get().HasConfig(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 85d87719a..cb22e3f14 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -648,6 +648,19 @@ void testFlipAfterGpuWork() { LibcRunShutdown_nid_postfix(); } +void testUnopenedEventRegistration() { + KernelEqueue eq = 0; + check(sceKernelCreateEqueue(&eq, "unopened") == 0, "the event queue was not created"); + check(sceVideoOutAddFlipEvent(eq, 0, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event on an unopened port was accepted"); + check(sceVideoOutAddVblankEvent(eq, 1, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "vblank event on an unopened port was accepted"); + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + check(sceVideoOutAddFlipEvent(eq, handle, nullptr) == 0, "flip event on an open port was refused"); + sceVideoOutClose(handle); + check(sceVideoOutAddFlipEvent(eq, handle, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event on a closed port was accepted"); + check(sceKernelDeleteEqueue(eq) == 0, "the event queue was not deleted"); + LibcRunShutdown_nid_postfix(); +} + void testOneDevice() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); @@ -693,6 +706,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "aftergpu") testFlipAfterGpuWork(); else if (argc == 2 && std::string(argv[1]) == "unavailable") testUnavailable(); else if (argc == 2 && std::string(argv[1]) == "onedevice") testOneDevice(); + else if (argc == 2 && std::string(argv[1]) == "unopened") testUnopenedEventRegistration(); else if (argc == 2 && std::string(argv[1]) == "overlay") testOverlay(); else if (argc == 2 && std::string(argv[1]) == "deferred") testDeferredBuffer(); else if (argc == 2 && std::string(argv[1]) == "unregistered") testUnregisteredBuffer(); From 351ca3bb12a86422b5897e1aff6ac07b098fc399 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sun, 11 Oct 2026 23:29:08 +0300 Subject: [PATCH 130/318] fix(libkernel): return EDEADLK for a thread joining itself and EINVAL for a null thread scePthreadJoin threw when a joinable thread joined itself and scePthreadJoin and scePthreadDetach threw on a null thread, so pthread_join and pthread_detach ended the process where FreeBSD libthr returns EDEADLK and EINVAL. --- core/libs/prx/libkernel/Pthread/src/Thread.cpp | 7 +++---- core/libs/tests/GuestThreadSelf.cpp | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index fada8e72a..d52679aaf 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -618,10 +618,9 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread } int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { - if (!thread) throw std::runtime_error("scePthreadJoin: null thread"); + if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; - if (thread == CurrentGuestState().current) - throw std::runtime_error("scePthreadJoin: cannot join current thread"); + if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; ThreadCancel::Check(); { std::unique_lock lock(thread->_join_mtx); @@ -642,7 +641,7 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { } int APS5_VABI scePthreadDetach(Pthread thread) { - if (!thread) throw std::runtime_error("scePthreadDetach: null thread"); + if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; thread->_detached = true; #ifdef _WIN32 diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 57413d0a9..9c59471e9 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -7,6 +7,8 @@ extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI scePthreadDetach(Pthread thread); +int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); +int APS5_VABI pthread_detach_nid_postfix(Pthread thread); void APS5_VABI scePthreadExit(void* retval); Pthread APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadGetthreadid(void); @@ -46,6 +48,9 @@ static bool StackContains(Pthread thread, const void* address) { static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; +static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000b; +static constexpr int GUEST_EDEADLK = 11; +static constexpr int GUEST_EINVAL = 22; static constexpr int MUTEX_TYPE_RECURSIVE = 2; static constexpr int PTHREAD_CANCEL_ENABLE = 0; static constexpr int PTHREAD_CANCEL_ASYNCHRONOUS = 2; @@ -69,6 +74,8 @@ struct WorkerContext { PthreadMutex* mutex = nullptr; int unlockResult = 0; bool testcancelReturned = false; + int sceJoinSelf = 0; + int posixJoinSelf = 0; }; static void* APS5_VABI Worker(void* arg) { @@ -80,6 +87,8 @@ static void* APS5_VABI Worker(void* arg) { int local = 0; context.workerStackReported = StackContains(context.selfFromWorker, &local); context.unlockResult = scePthreadMutexUnlock(context.mutex); + context.sceJoinSelf = scePthreadJoin(context.selfFromWorker, nullptr); + context.posixJoinSelf = pthread_join_nid_postfix(context.selfFromWorker, nullptr); int oldState = -1; int oldType = -1; context.testcancelReturned = scePthreadSetcancelstate(PTHREAD_CANCEL_ENABLE, &oldState) == SCE_OK && @@ -103,6 +112,10 @@ int main() { Require(StackContains(mainSelf, &local)); Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadDetach(mainSelf) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadJoin(nullptr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadDetach(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_join_nid_postfix(nullptr, nullptr) == GUEST_EINVAL); + Require(pthread_detach_nid_postfix(nullptr) == GUEST_EINVAL); PthreadMutexattr attr = nullptr; Require(scePthreadMutexattrInit(&attr) == SCE_OK); @@ -129,6 +142,8 @@ int main() { Require(context.workerThreadId == static_cast(context.workerTid)); Require(context.workerPosixThreadId == context.workerThreadId && context.workerPosixThreadId != static_cast(mainTid)); Require(context.unlockResult == SCE_KERNEL_ERROR_EPERM); + Require(context.sceJoinSelf == SCE_KERNEL_ERROR_EDEADLK); + Require(context.posixJoinSelf == GUEST_EDEADLK); Require(context.workerStackReported); Require(context.testcancelReturned); From 409f53eaed31b12761b58c158d4a75e69389c1f9 Mon Sep 17 00:00:00 2001 From: ethan Date: Sun, 11 Oct 2026 15:24:53 -0700 Subject: [PATCH 131/318] fix(relinker): filter non-reference RIP-relative FF operations --- core/relinker/CMakeLists.txt | 12 ++++++ .../src/analysis/CallSiteResolver.cpp | 3 +- .../relinker/tests/CallSiteResolverTests.cpp | 41 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 core/relinker/relinker/tests/CallSiteResolverTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 095915729..ce383621e 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -303,6 +303,18 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/io/include ) add_test(NAME amd64_only_converter COMMAND amd64_only_converter_tests) + add_test_executable(call_site_resolver_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteResolverTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_resolver_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ) + add_test(NAME call_site_resolver COMMAND call_site_resolver_tests) add_test_executable(decoder_length_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/DecoderLengthTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 6f08ab26f..5dafdf912 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -27,7 +27,8 @@ bool isRipRelativeFF(const std::vector& text, const Codegen::Instr std::uint8_t modrm = text[cur + 1]; std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; std::uint8_t rm = modrm & ModRmRmMask; - return mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const auto operation = (modrm >> ModRmRegShift) & ModRmRegMask; + return mod == ModRmModIndirect && rm == ModRmRmRipRelative && (operation >= 2 && operation <= 6); } break; } diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp new file mode 100644 index 000000000..f780307eb --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const std::string& name) { + if (!condition) throw std::runtime_error(name); +} + +void check(const std::string& name, std::initializer_list instruction, bool expected) { + auto resolver = Relinker::MakeCallSiteResolver(); + const std::vector text(instruction); + const auto sites = resolver->ResolveCallSites(text, 0x1000, 0x1100, 8); + require(sites == (expected ? std::vector{0x1000} : std::vector{}), name); + require(resolver->ResolveCallSites(text, 0x1000, 0x1200, 8).empty(), name + " unexpected address"); +} + +} + +int main() { + try { + check("indirect call", {0xff, 0x15, 0xfa, 0, 0, 0}, true); + check("indirect tail jump", {0xff, 0x25, 0xfa, 0, 0, 0}, true); + check("far indirect call", {0xff, 0x1d, 0xfa, 0, 0, 0}, true); + check("far indirect jump", {0xff, 0x2d, 0xfa, 0, 0, 0}, true); + check("RIP-relative function pointer load", {0x48, 0x8b, 0x05, 0xf9, 0, 0, 0}, true); + check("increment memory", {0xff, 0x05, 0xfa, 0, 0, 0}, false); + check("decrement memory", {0xff, 0x0d, 0xfa, 0, 0, 0}, false); + check("push memory", {0xff, 0x35, 0xfa, 0, 0, 0}, true); + std::cout << "Call-site resolver tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "call_site_resolver_tests: " << error.what() << '\n'; + return 1; + } +} From 233590462c94076cfdb3289fd2dce33797ea9879 Mon Sep 17 00:00:00 2001 From: Simon <340958984+ee6aba54@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:40:46 +0200 Subject: [PATCH 132/318] fix(libSceNgs2): report EMPTY for a streaming sampler that played its queue A playing sampler voice whose waveform still accepts blocks (CONTINUE) reported only INUSE|PLAYING after it had played every queued block, so a title that waits for EMPTY before it reuses the voice never did. PPSA18391 (engine: Clickteam Fusion) reads state_flags & 0x22 != 2 from sceNgs2VoiceGetState as the end of a sound; each of its sound effects played once and never again. The voice now also reports EMPTY (0x20) in that state and keeps playing, so blocks added later still play. A waveform that was closed without CONTINUE does not report it. --- .../libSceNgs2.native/include/Ngs2Types.hpp | 1 + .../libs/prx/libSceNgs2.native/src/Render.cpp | 1 + .../libSceNgs2.native/tests/Ngs2Stream.cpp | 27 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 30 insertions(+) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index c79d3ad7c..f7931d83f 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -36,6 +36,7 @@ static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_INUSE = 1; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_PLAYING = 2; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_PAUSED = 4; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_STOPPED = 8; +static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_EMPTY = 0x20; static constexpr std::uint32_t SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS = 0x0001; static constexpr std::uint32_t SCE_NGS2_VOICE_PARAM_PORT_VOLUME = 0x0002; diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index c93902179..6cf69f714 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -314,6 +314,7 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo for (auto* voice : voices) { if (voice->state == Ngs2PlayState::Stopped) voice->state = Ngs2PlayState::Empty; voice->stateFlags = static_cast(voice->state); + if (voice->state == Ngs2PlayState::Playing && voice->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER && voice->blocks.empty() && voice->acceptsBlocks) voice->stateFlags |= SCE_NGS2_VOICE_STATE_FLAG_EMPTY; } system.renderCount++; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp index af1e0b06e..726cf9148 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp @@ -158,10 +158,37 @@ static void TestSilenceBlock() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestDrainedStreamReportsEmpty() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = StreamVoice(rack, 0, master); + const std::vector pcm(4, 16384); + constexpr std::uint32_t playing = SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING; + + AddBlock(voice, pcm.data(), pcm.size() * 2, 4, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + RenderI16(system); + Require(Flags(voice) == (playing | SCE_NGS2_VOICE_STATE_FLAG_EMPTY)); + + const std::vector longer(Grain * 2, 8192); + AddBlock(voice, longer.data(), longer.size() * 2, Grain * 2, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); + const auto out = RenderI16(system); + Require(out[0] == 8192); + Require(Flags(voice) == playing); + + AddBlock(voice, pcm.data(), pcm.size() * 2, 4, 0); + RenderI16(system); + RenderI16(system); + Require((Flags(voice) & SCE_NGS2_VOICE_STATE_FLAG_EMPTY) == 0); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestStreamEndsAtDeclaredSamples(); TestStreamClosedBeforeDeclaredSamples(); TestStreamRejections(); TestSilenceBlock(); + TestDrainedStreamReportsEmpty(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index face0cf0e..6e7eced0d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -257,6 +257,7 @@ Throughout the project, every function at every stage either **does exactly what - [NGS2 silent waveform blocks](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw - [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. `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. - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - the PS5 init data layout (`this_size` 0x230: auto start at 0x74, nine 0x30-byte thread parameter blocks from 0x78, output video frame buffer count at 0x228) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/avPlayer.cpp), [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/video/avplayer.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Media/AvPlayer.cs) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs), which agree on it. PPSA25063 fills the same fields, writes auto start 0 and starts playback with `sceAvPlayerStart`; a title that sets auto start has not been seen. A smaller `this_size` is read with the PS4 offsets. The thread parameters are ignored From 9a0c59570c63c432075136a14738673c6af610a4 Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd Date: Sun, 11 Oct 2026 21:17:49 +0400 Subject: [PATCH 133/318] feat(ajm): decode Opus surround mapping family --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 34 +++++++- core/libs/tests/GuestAjm.cpp | 90 +++++++++++++++++++++- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 4d7cd26fa..8dc5f2ad7 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -6,6 +6,7 @@ extern "C" { #include #include +#include } #include #include @@ -134,6 +135,7 @@ struct Instance { std::uint32_t mp3Bitrate = 0; AVCodecContext* opus = nullptr; std::uint32_t opusChannels = 0; + std::uint32_t opusMappingFamily = 0; std::uint32_t opusSampleRate = 0; std::vector opusPending; Resampler resampler; @@ -369,10 +371,13 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete std::memcpy(&channels, parameters, sizeof(channels)); std::memcpy(&sampleRate, parameters + 4, sizeof(sampleRate)); if (channels == 0 || channels > 8 || sampleRate == 0) return AJM_RESULT_INVALID_PARAMETER; - std::uint32_t third = 0; - if (size >= 12) std::memcpy(&third, parameters + 8, sizeof(third)); - if (sampleRate != 48000 || third != 0) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000 or nonzero third parameter word)"); + std::uint32_t mappingFamily = 0; + if (size >= 12) std::memcpy(&mappingFamily, parameters + 8, sizeof(mappingFamily)); + if (sampleRate != 48000) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000)"); + if (mappingFamily > 1) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0 or 1)"); + if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; + instance.opusMappingFamily = mappingFamily; instance.opusSampleRate = sampleRate; OpenOpus(instance); instance.opusPending.clear(); @@ -948,6 +953,29 @@ void OpenOpus(Instance& instance) { if (!instance.opus) throw std::runtime_error("AJM: cannot allocate the FFmpeg Opus decoder"); av_channel_layout_default(&instance.opus->ch_layout, static_cast(instance.opusChannels)); instance.opus->sample_rate = 48000; + if (instance.opusMappingFamily == 1) { + static constexpr std::uint8_t streams[8] = {1, 1, 2, 2, 3, 4, 4, 5}; + static constexpr std::uint8_t coupled[8] = {0, 1, 1, 2, 2, 2, 3, 3}; + static constexpr std::uint8_t mapping[8][8] = { + {0}, {0, 1}, {0, 2, 1}, {0, 1, 2, 3}, + {0, 4, 1, 2, 3}, {0, 4, 1, 2, 3, 5}, + {0, 4, 1, 2, 3, 5, 6}, {0, 6, 1, 2, 3, 4, 5, 7} + }; + const std::size_t index = instance.opusChannels - 1; + instance.opus->extradata_size = static_cast(21 + instance.opusChannels); + instance.opus->extradata = static_cast(av_mallocz(instance.opus->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE)); + if (!instance.opus->extradata) throw std::bad_alloc(); + auto* header = instance.opus->extradata; + std::memcpy(header, "OpusHead", 8); + header[8] = 1; + header[9] = static_cast(instance.opusChannels); + header[12] = 0x80; + header[13] = 0xbb; + header[18] = 1; + header[19] = streams[index]; + header[20] = coupled[index]; + std::memcpy(header + 21, mapping[index], instance.opusChannels); + } if (avcodec_open2(instance.opus, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg Opus decoder"); } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index c35ca5e78..b03e0a14f 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -131,10 +131,10 @@ void RunDecode(std::uint32_t context, std::uint32_t instance, const std::uint8_t Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); } -void TestOpus(std::uint32_t context) { +void TestOpus(std::uint32_t context, std::uint32_t mappingFamily = 0) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); - const std::uint32_t parameters[3] = {2, 48000, 0}; + const std::uint32_t parameters[3] = {2, 48000, mappingFamily}; std::vector batch(4096); AjmBatchInfo info{}; std::int64_t initResult[2] = {-1, -1}; @@ -171,6 +171,80 @@ void TestOpus(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestOpusSurround(std::uint32_t context, std::uint32_t channels, std::size_t activeStream) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {channels, 48000, 1}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), initResult) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(initResult[0] == 0); + const std::size_t streamCount = channels == 3 || channels == 4 ? 2 : channels == 5 ? 3 : channels == 8 ? 5 : 4; + const std::size_t coupledCount = channels == 3 ? 1 : channels >= 7 ? 3 : 2; + std::size_t offset = 0; + for (std::size_t packetIndex = 0; packetIndex < 3; ++packetIndex) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + Require((OPUS_STEREO[offset + 2] & 3u) == 0 && bytes < 252); + std::vector packet{0, 0}; + for (std::size_t stream = 0; stream < streamCount; ++stream) { + if (stream == activeStream) { + packet.push_back(OPUS_STEREO[offset + 2]); + if (stream + 1 < streamCount) packet.push_back(static_cast(bytes - 1)); + packet.insert(packet.end(), OPUS_STEREO + offset + 3, OPUS_STEREO + offset + 2 + bytes); + } else { + packet.push_back(stream < coupledCount ? 0xfc : 0xf8); + if (stream + 1 < streamCount) packet.push_back(2); + packet.push_back(0xff); + packet.push_back(0xfe); + } + } + const std::size_t payload = packet.size() - 2; + packet[0] = static_cast(payload); + packet[1] = static_cast(payload >> 8u); + std::vector pcm(512 * channels, -12345); + DecodeSideband first{}; + RunDecode(context, instance, packet.data(), packet.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), first); + Require(first.result == 0 && static_cast(first.inputConsumed) == packet.size() && first.outputWritten == static_cast(pcm.size() * sizeof(std::int16_t))); + std::vector rest(448 * channels, -12345); + DecodeSideband remaining{}; + RunDecode(context, instance, packet.data() + packet.size(), 0, rest.data(), rest.size() * sizeof(std::int16_t), remaining); + Require(remaining.result == 0 && remaining.inputConsumed == 0 && remaining.outputWritten == static_cast(rest.size() * sizeof(std::int16_t))); + Require(remaining.totalDecodedSamples == (packetIndex + 1) * 960); + pcm.insert(pcm.end(), rest.begin(), rest.end()); + for (std::size_t channel = 0; channel < channels; ++channel) { + int peak = 0; + for (std::size_t sample = channel; sample < pcm.size(); sample += channels) peak = std::max(peak, std::abs(static_cast(pcm[sample]))); + const std::size_t activeChannel = activeStream == 0 ? 0 : channels == 4 ? 2 : channels == 5 ? 3 : channels == 6 ? 4 : channels == 7 ? activeStream == 1 ? 5 : 2 : activeStream == 1 ? 6 : 4; + const std::size_t secondChannel = channels == 7 && activeStream == 2 ? 4 : activeChannel + 1; + const bool active = channel == activeChannel || channel == secondChannel; + Require(active ? packetIndex == 0 || peak > 900 : peak <= 2); + } + offset += 2 + bytes; + } + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestOpusFamilyZeroChannels(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {4, 48000, 0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), result) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(result[0] == 4 && result[1] == 0); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestOpusInputBeyondStatedData(std::uint32_t context, bool multipleFrames) { #ifdef _WIN32 SYSTEM_INFO system{}; @@ -1170,6 +1244,18 @@ int main() { TestMp3ParseOfl(); TestMp3(context); TestOpus(context); + TestOpus(context, 1); + TestOpusFamilyZeroChannels(context); + TestOpusSurround(context, 3, 0); + for (std::size_t stream = 0; stream < 2; ++stream) { + TestOpusSurround(context, 4, stream); + TestOpusSurround(context, 5, stream); + TestOpusSurround(context, 6, stream); + } + for (std::size_t stream = 0; stream < 3; ++stream) { + TestOpusSurround(context, 7, stream); + TestOpusSurround(context, 8, stream); + } TestOpusInputBeyondStatedData(context, false); TestOpusInputBeyondStatedData(context, true); TestDecodeSingle(context); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index face0cf0e..3724f0fa0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -269,7 +269,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [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 - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts -- [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted +- [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. Only 48000 Hz and mapping families 0 and 1 are implemented; other rates and families throw - [sceAjmBatchJobSetResampleParametersEx](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is unknown and throws - [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw - [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does From 859d0b1e630654bc9ade945457028b40a2db52ef Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:33:41 +0200 Subject: [PATCH 134/318] fix(exports): also export sceHmd2GetFieldOfView main has sceHmd2GetFieldOfViewWithoutHandle only; PPSA07411 imports sceHmd2GetFieldOfView. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceHmd2/Export.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index db43b00f6..a6ca6f3ad 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -16,6 +16,11 @@ std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { return SCE_HMD2_ERROR_UNSUPPORTED_FEATURE; } +int APS5_VABI sceHmd2GetFieldOfView() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2GetDeviceInformationByHandle() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5a698d42d..28b660e1f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -626,7 +626,7 @@ Throughout the project, every function at every stage either **does exactly what - PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, sceSslConnect, sceSslRead, sceSslWrite](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. -- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2GetDeviceInformationByHandle and sceHmd2Terminate](../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) +- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2GetDeviceInformationByHandle, sceHmd2GetFieldOfView and sceHmd2Terminate](../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) - [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [fstatvfs](../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) From ac4954c6e30523173a14a3da7a525d6119ba525f Mon Sep 17 00:00:00 2001 From: Mouhand Kaddo <73709175+Mouhand-Kaddo@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:33:53 +0400 Subject: [PATCH 135/318] fix(libkernel): guard Unix host polling in Windows select --- core/libs/prx/libkernel/Socket/src/Socket.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 3f71f4f1c..89c4f9c26 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -186,6 +186,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* waitMilliseconds = static_cast(std::clamp(roundedMilliseconds, 0, 100)); } if (devicesReady) waitMilliseconds = 0; +#ifndef _WIN32 if (!hosts.empty() && !sockets.empty()) { if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); const auto sliceEnd = std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds); @@ -214,7 +215,9 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* return -1; } for (std::size_t index = 0; index < hosts.size(); ++index) requested[hostSlots[index]].revents = hosts[index].revents; - } else if (!sockets.empty()) { + } else +#endif + if (!sockets.empty()) { if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); for (auto& socket : sockets) socket.revents = 0; const int result = poller(sockets.data(), static_cast(sockets.size()), waitMilliseconds); From 36c78ba1433e467d777595441067916cc38873b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 22:34:39 +0000 Subject: [PATCH 136/318] fix(relinker): report the ModRM of 3DNow! instructions in decoded instruction info DecodeInstruction did not treat 0F 0F as having a ModRM byte, so a RIP-relative 3DNow! operand was not reported to the analyses and the --to-intel stub relocation that read HasRipRelativeDisp. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VZWLdTjf1S57iLgn2rcaFb --- core/relinker/codegen/src/x86/X64InstructionDecoder.cpp | 2 +- core/relinker/codegen/tests/DecoderLengthTests.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index ab5785a01..4f9ff5b3b 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -556,7 +556,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( op == TwoByteImulRmModRm || op == TwoByteGrp7 || op == TwoByteGrp15 || op == TwoByteXadd || op == TwoByteGrp9 || op == TwoByteNopModRm || - op == TwoByteEndbr || op == TwoByteMovImm8ModRm || + op == TwoByteEndbr || op == TwoByteMovImm8ModRm || op == TwoByte3DNow || op == ThreeByteEscape38 || op == ThreeByteEscape3A || (op >= TwoByteShiftImm8Min && op <= TwoByteShiftImm8Max) || op == TwoByteShldImm8 || op == TwoByteShrdImm8 || diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index d119b3b32..e141e1539 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -77,6 +77,10 @@ int main() { } catch (const Codegen::CodegenException&) { } } + const auto threeDNow = parse("0f0f0d11223344a6"); + const auto threeDNowInfo = decoder.DecodeInstruction(threeDNow.data(), threeDNow.size()); + require(threeDNowInfo.HasModRm && threeDNowInfo.ModRmByte == 0x0d, "3DNow! ModRM was not reported"); + require(threeDNowInfo.HasRipRelativeDisp && threeDNowInfo.RipRelativeDispOffset == 3, "3DNow! RIP-relative displacement was not reported"); std::cout << "Decoder length tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 567898cd587d7f3d58456c8f2fd87f85d8379d10 Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:39:28 +0200 Subject: [PATCH 137/318] feat(libSceSysmodule): register libSceVoiceChat module ID (0x114) --- core/libs/prx/libSceSysmodule/ModuleTable.hpp | 1 + core/libs/tests/GuestSysmodule.cpp | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index 14642ee07..0a8a13d83 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -152,6 +152,7 @@ inline const std::unordered_map kModuleTable = { {0x00000110, "libSceNpTrophy2"}, {0x00000112, "libSceNpSessionSignaling"}, {0x00000113, "libSceNpEntitlementAccess"}, + {0x00000114, "libSceVoiceChat"}, {0x00000115, "libSceNpCppWebApi"}, {0x00000116, "libSceHubAppUtil"}, {0x0000011a, "libSceNpPartner001"}, diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 77ec859f5..b83d037bb 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -44,6 +44,7 @@ constexpr int kInvalidValue = static_cast(0x805A1000); constexpr int kModuleUnloaded = static_cast(0x805A1001); constexpr std::uint32_t kInternalFlag = 0x80000000u; constexpr std::uint16_t kTrophy2ModuleId = 0x0110; +constexpr std::uint16_t kVoiceChatModuleId = 0x0114; constexpr std::uint32_t kAgcModuleId = 0x80000094; #ifdef __linux__ @@ -141,6 +142,13 @@ int main() { Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == kModuleUnloaded); Require(sceSysmoduleUnloadModuleInternal(kTrophy2ModuleId) == kModuleUnloaded); + Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kVoiceChatModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kVoiceChatModuleId) == 0); + Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == 0); + Require(sceSysmoduleUnloadModule(kVoiceChatModuleId) == 0); + Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); From 0a324d8ccb47f79b3f513318aef1d67f2a1aa4d0 Mon Sep 17 00:00:00 2001 From: ethan Date: Sun, 11 Oct 2026 15:45:26 -0700 Subject: [PATCH 138/318] fix(relinker): resolve .suprx guest module aliases --- .../relinker/src/guest/GuestModuleBuilder.cpp | 2 +- .../tests/test_guest_needed_modules.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index d51c4c160..8d1ddeb71 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -44,7 +44,7 @@ std::string FoldFilename(std::string name) { std::string ModuleStem(std::string name, const bool windows) { if (windows) name = FoldFilename(std::move(name)); - for (const std::string_view suffix : {".debug_prx", ".sprx", ".prx"}) { + for (const std::string_view suffix : {".debug_prx", ".suprx", ".sprx", ".prx"}) { if (name.size() > suffix.size() && name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); } return {}; diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index adc9b97da..d15b96ac9 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -142,6 +142,24 @@ def main(): needed = needed_libraries(output.read_bytes()) assert needed == [f"$ORIGIN/app0/{directory}/needed.prx.guest.prx"], needed + for requested, physical in ((b"needed.suprx", "needed.prx"), + (b"needed.prx", "needed.suprx"), + (b"needed.sprx", "needed.suprx"), + (b"needed.debug_prx", "needed.suprx")): + case = work / f"{windows}-suprx-alias-{requested.decode()}-{physical}" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / physical).write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, requested) + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = case / "app0" / "sce_module" / (physical + ".guest.prx") + assert artifact.is_file(), (requested, physical, result.stdout) + if not windows: + needed = needed_libraries(output.read_bytes()) + assert needed == [f"$ORIGIN/app0/sce_module/{physical}.guest.prx"], needed + elif os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + if windows: case = work / "debug-name-case" (case / "prx").mkdir(parents=True) From 3ba4751f98c2d9415133baa2d7f9e84c26c3e2a6 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 19:31:11 -0300 Subject: [PATCH 139/318] fix(libkernel): report the console's scheduling priority range On a PS5 (firmware 13.60) sched_get_priority_max returns 767 and sched_get_priority_min 256 for SCHED_FIFO and SCHED_RR; the two were swapped. Every other policy, SCHED_OTHER included, returns -1 with EINVAL instead of a range or a host exception. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libkernel/System/src/Process.cpp | 19 ++++++++++++------- core/libs/tests/GuestSchedPriority.cpp | 14 +++++++++----- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index f4d546837..1f82c94c5 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -107,9 +107,8 @@ ProcessArguments& getProcessArguments() { return arguments; } -void validateSchedulingPolicy(int policy) { - if (policy < 1 || policy > 3) - throw std::invalid_argument("Unsupported guest scheduling policy"); +bool hasPriorityRange(int policy) { + return policy == 1 || policy == 3; } constexpr int guestFault = 14; @@ -362,13 +361,19 @@ void APS5_VABI sync_nid_postfix(void) { } int APS5_VABI sched_get_priority_max_nid_postfix(int policy) { - validateSchedulingPolicy(policy); - return 256; + if (!hasPriorityRange(policy)) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + return 767; } int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { - validateSchedulingPolicy(policy); - return 767; + if (!hasPriorityRange(policy)) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + return 256; } int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index 7f135f5ef..0e9d418d1 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -1,19 +1,23 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" { int APS5_VABI sched_get_priority_max_nid_postfix(int policy); int APS5_VABI sched_get_priority_min_nid_postfix(int policy); } static void Require(bool value) { if (!value) std::abort(); } static bool Rejects(int policy) { - try { sched_get_priority_max_nid_postfix(policy); } catch (const std::invalid_argument&) { return true; } - return false; + *__error_nid_postfix() = 0; + const bool max = sched_get_priority_max_nid_postfix(policy) == -1 && *__error_nid_postfix() == 22; + *__error_nid_postfix() = 0; + return max && sched_get_priority_min_nid_postfix(policy) == -1 && *__error_nid_postfix() == 22; } int main() { - for (int policy : {1, 2, 3}) { - Require(sched_get_priority_max_nid_postfix(policy) == 256); - Require(sched_get_priority_min_nid_postfix(policy) == 767); + Require(Rejects(-1) && Rejects(2) && Rejects(5)); + for (int policy : {1, 3}) { + Require(sched_get_priority_max_nid_postfix(policy) == 767); + Require(sched_get_priority_min_nid_postfix(policy) == 256); } Require(Rejects(0) && Rejects(4)); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 59ccac655..075670ea6 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -360,7 +360,7 @@ Throughout the project, every function at every stage either **does exactly what - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes - [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes - [sceVideoOutAddPreVblankStartEvent](../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data -- [sched_get_priority_max, sched_get_priority_min](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `SCHED_OTHER` (2) reports the same 256 to 767 range as `SCHED_FIFO` and `SCHED_RR`; the console's answer is unverified. FreeBSD accepts `SCHED_OTHER` with its own range, and shadPS4 rejects it with `EINVAL`. `pthread_setschedparam` ignores the policy, so a priority taken from this range applies +- [sched_get_priority_max, sched_get_priority_min](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - measured on a PS5 (firmware 13.60): `SCHED_FIFO` (1) and `SCHED_RR` (3) report a maximum of 767 and a minimum of 256, and every other policy, `SCHED_OTHER` (2) included, returns -1 with `EINVAL`. `pthread_setschedparam` ignores the policy and does not check the priority, where the console rejects `SCHED_OTHER` and priorities below 256 with `EINVAL` - [inet_pton, inet_ntop](../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library - [gethostbyname](../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver From 5fd0e261c93f952b18072837da3c041de9b88603 Mon Sep 17 00:00:00 2001 From: kallmek3nny Date: Sun, 11 Oct 2026 18:48:18 -0400 Subject: [PATCH 140/318] fix(aio): use precise polling sleep --- core/libs/prx/libkernel/Aio/src/Aio.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index a89f42561..c16167088 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -6,10 +6,10 @@ #include #include #include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/PreciseWait.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -273,7 +273,7 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); if (elapsed > static_cast(*usec)) return SCE_KERNEL_ERROR_ETIMEDOUT; } - std::this_thread::sleep_for(std::chrono::microseconds(10)); + PreciseSleepUs(10); } } @@ -298,7 +298,7 @@ int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec return SCE_KERNEL_ERROR_ETIMEDOUT; } } - std::this_thread::sleep_for(std::chrono::microseconds(10)); + PreciseSleepUs(10); } } From 7b5ac83a2a92b47b0e8042531c75586be0846445 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 19:40:48 -0300 Subject: [PATCH 141/318] fix(libkernel): destroy rwlocks the way the console does On a PS5 (firmware 13.60) destroying a rwlock returns 0 even while it is held, and leaves 1 in the handle; every later call on that handle returns EINVAL until it is initialized again. A zero handle is initialized on first use by the scePthreadRwlock calls too. Here a held rwlock returned EBUSY, a destroyed handle became null and was silently re-created by the next POSIX call, and the scePthreadRwlock calls threw on a zero handle. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libkernel/Pthread/src/Rwlock.cpp | 40 ++++++-- core/libs/tests/GuestPthreadRwlock.cpp | 93 +++++++++++-------- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 88 insertions(+), 47 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index cd8890dcd..3ff9b3088 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -2,6 +2,7 @@ #include "../include/Rwlock.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include #include #include #include @@ -12,10 +13,22 @@ static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000B; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; + +static PthreadRwlockPrivate* const DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); static PthreadRwlockPrivate* RequireRwlock(PthreadRwlock* rwlock, const char* funcName) { - if (!rwlock || !*rwlock) throw std::runtime_error(std::string(funcName) + ": null rwlock"); - return *rwlock; + if (!rwlock) throw std::runtime_error(std::string(funcName) + ": null rwlock"); + std::atomic_ref slot(*rwlock); + PthreadRwlock current = slot.load(std::memory_order_acquire); + if (current == nullptr) { + auto* created = new PthreadRwlockPrivate(); + if (slot.compare_exchange_strong(current, created, std::memory_order_acq_rel, std::memory_order_acquire)) + current = created; + else + delete created; + } + return current == DestroyedRwlock ? nullptr : current; } static bool OwnsWrite(const PthreadRwlockPrivate* lock) { @@ -25,12 +38,16 @@ static bool OwnsWrite(const PthreadRwlockPrivate* lock) { extern "C" { int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock) { - auto* lock = RequireRwlock(rwlock, __func__); - if (lock->_writer.load(std::memory_order_acquire) != std::thread::id{} || - lock->_readers.load(std::memory_order_acquire) != 0) - return SCE_KERNEL_ERROR_EBUSY; - delete lock; - *rwlock = nullptr; + if (!rwlock) throw std::runtime_error("scePthreadRwlockDestroy: null rwlock"); + std::atomic_ref slot(*rwlock); + PthreadRwlock lock = slot.load(std::memory_order_acquire); + do { + if (lock == nullptr) return SCE_OK; + if (lock == DestroyedRwlock) return SCE_KERNEL_ERROR_EINVAL; + } while (!slot.compare_exchange_weak(lock, DestroyedRwlock, std::memory_order_acq_rel, std::memory_order_acquire)); + if (lock->_writer.load(std::memory_order_acquire) == std::thread::id{} && + lock->_readers.load(std::memory_order_acquire) == 0) + delete lock; return SCE_OK; } @@ -46,6 +63,7 @@ int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockatt int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; #ifdef _WIN32 while (!lock->_lock.try_lock_shared()) TimedWait::SleepNanos(1000000); @@ -58,6 +76,7 @@ int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EBUSY; if (!lock->_lock.try_lock_shared()) return SCE_KERNEL_ERROR_EBUSY; lock->_readers.fetch_add(1, std::memory_order_release); @@ -66,6 +85,7 @@ int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock_shared(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_shared_for(std::chrono::microseconds(micros)); }); if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; @@ -75,6 +95,7 @@ int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; #ifdef _WIN32 while (!lock->_lock.try_lock()) TimedWait::SleepNanos(1000000); @@ -87,6 +108,7 @@ int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock) || !lock->_lock.try_lock()) return SCE_KERNEL_ERROR_EBUSY; lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); return SCE_OK; @@ -94,6 +116,7 @@ int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_for(std::chrono::microseconds(micros)); }); if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; @@ -103,6 +126,7 @@ int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) { lock->_writer.store(std::thread::id{}, std::memory_order_release); lock->_lock.unlock(); diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index ff27c538e..145f8e802 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -23,12 +23,16 @@ int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, cons int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime); int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockattr* attr, const char* name); int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); } using TimedLock = int (APS5_VABI *)(PthreadRwlock*, const KernelTimespec*); static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_EBUSY = 16; @@ -39,6 +43,8 @@ static constexpr std::int64_t NANOS_PER_SECOND = 1000000000; static void Require(bool value) { if (!value) std::abort(); } +static const PthreadRwlock DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); + static KernelTimespec After(std::int64_t millis) { KernelTimespec now{}; Require(clock_gettime_nid_postfix(GUEST_REALTIME_CLOCK, &now) == 0); @@ -151,19 +157,44 @@ static void CheckWriterHandoff() { Require(scePthreadCreate(&state.firstThread, nullptr, HandoffFirst, &state, nullptr) == SCE_OK); Require(scePthreadCreate(&state.secondThread, nullptr, HandoffSecond, &state, nullptr) == SCE_OK); while (!state.firstHeld.load()) std::this_thread::yield(); - Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&state.rwlock) == SCE_KERNEL_ERROR_EBUSY); Require(state.rwlock != nullptr); state.firstRelease.store(true); Require(scePthreadJoin(state.firstThread, nullptr) == SCE_OK); while (!state.secondAcquired.load()) std::this_thread::yield(); - Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&state.rwlock) == SCE_KERNEL_ERROR_EBUSY); Require(state.rwlock != nullptr); state.secondRelease.store(true); Require(scePthreadJoin(state.secondThread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == 0); - Require(state.rwlock == nullptr); + Require(state.rwlock == DestroyedRwlock); +} + +static void CheckDestroyLifecycle() { + PthreadRwlock held = nullptr; + Require(scePthreadRwlockInit(&held, nullptr, nullptr) == SCE_OK); + Require(scePthreadRwlockWrlock(&held) == SCE_OK); + Require(scePthreadRwlockDestroy(&held) == SCE_OK); + Require(held == DestroyedRwlock); + Require(scePthreadRwlockUnlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockRdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockTryrdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockDestroy(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_rwlock_rdlock_nid_postfix(&held) == GUEST_EINVAL); + Require(held == DestroyedRwlock); + Require(scePthreadRwlockInit(&held, nullptr, nullptr) == SCE_OK); + Require(held != nullptr && held != DestroyedRwlock); + Require(scePthreadRwlockRdlock(&held) == SCE_OK); + Require(scePthreadRwlockRdlock(&held) == SCE_OK); + Require(scePthreadRwlockDestroy(&held) == SCE_OK); + Require(held == DestroyedRwlock); + + PthreadRwlock zero = nullptr; + Require(scePthreadRwlockDestroy(&zero) == SCE_OK); + Require(zero == nullptr); + Require(scePthreadRwlockRdlock(&zero) == SCE_OK); + Require(zero != nullptr && zero != DestroyedRwlock); + Require(scePthreadRwlockUnlock(&zero) == SCE_OK); + Require(scePthreadRwlockDestroy(&zero) == SCE_OK); + Require(zero == DestroyedRwlock); } static void CheckFailedLockAttempts() { @@ -172,23 +203,20 @@ static void CheckFailedLockAttempts() { const KernelTimespec timeout = After(10); Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &timeout) == GUEST_EDEADLK); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); - Require(rwlock == nullptr); + Require(rwlock == DestroyedRwlock); + rwlock = nullptr; Holder reader{&rwlock, false}; Start(reader); const KernelTimespec past = After(-1000); Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &past) == GUEST_ETIMEDOUT); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); reader.release.store(true); Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); - Require(rwlock == nullptr); + Require(rwlock == DestroyedRwlock); } int main() { @@ -238,8 +266,6 @@ int main() { Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &invalid) == 0); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); ExpectTimeout(pthread_rwlock_timedwrlock_nid_postfix, &rwlock); deadline = After(5000); reader.release.store(true); @@ -252,8 +278,6 @@ int main() { Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); ExpectTimeout(pthread_rwlock_timedrdlock_nid_postfix, &rwlock); ExpectTimeout(pthread_rwlock_timedwrlock_nid_postfix, &rwlock); deadline = After(5000); @@ -268,58 +292,51 @@ int main() { catch (const std::runtime_error&) { rejected = true; } Require(rejected); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); - Require(rwlock == nullptr); + Require(rwlock == DestroyedRwlock); PthreadRwlock unheld = nullptr; Require(scePthreadRwlockInit(&unheld, nullptr, nullptr) == SCE_OK); Require(unheld != nullptr); Require(pthread_rwlock_destroy_nid_postfix(&unheld) == 0); - Require(unheld == nullptr); + Require(unheld == DestroyedRwlock); PthreadRwlock rd = nullptr; Require(pthread_rwlock_rdlock_nid_postfix(&rd) == 0); - Require(pthread_rwlock_destroy_nid_postfix(&rd) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rd) == SCE_KERNEL_ERROR_EBUSY); - Require(rd != nullptr); - Require(pthread_rwlock_unlock_nid_postfix(&rd) == 0); - Require(pthread_rwlock_tryrdlock_nid_postfix(&rd) == 0); - Require(pthread_rwlock_unlock_nid_postfix(&rd) == 0); Require(pthread_rwlock_destroy_nid_postfix(&rd) == 0); - Require(rd == nullptr); + Require(rd == DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&rd) == GUEST_EINVAL); + Require(pthread_rwlock_tryrdlock_nid_postfix(&rd) == GUEST_EINVAL); + Require(pthread_rwlock_destroy_nid_postfix(&rd) == GUEST_EINVAL); + Require(rd == DestroyedRwlock); PthreadRwlock wr = nullptr; Require(pthread_rwlock_wrlock_nid_postfix(&wr) == 0); - Require(pthread_rwlock_destroy_nid_postfix(&wr) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&wr) == SCE_KERNEL_ERROR_EBUSY); - Require(wr != nullptr); - Require(pthread_rwlock_unlock_nid_postfix(&wr) == 0); - Require(pthread_rwlock_trywrlock_nid_postfix(&wr) == 0); - Require(pthread_rwlock_unlock_nid_postfix(&wr) == 0); Require(pthread_rwlock_destroy_nid_postfix(&wr) == 0); - Require(wr == nullptr); + Require(wr == DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&wr) == GUEST_EINVAL); + Require(pthread_rwlock_trywrlock_nid_postfix(&wr) == GUEST_EINVAL); + Require(pthread_rwlock_destroy_nid_postfix(&wr) == GUEST_EINVAL); + Require(wr == DestroyedRwlock); PthreadRwlock remoteRd = nullptr; Holder bgReader{&remoteRd, false}; Start(bgReader); - Require(pthread_rwlock_destroy_nid_postfix(&remoteRd) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&remoteRd) == SCE_KERNEL_ERROR_EBUSY); Require(remoteRd != nullptr); bgReader.release.store(true); Require(scePthreadJoin(bgReader.thread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&remoteRd) == 0); - Require(remoteRd == nullptr); + Require(remoteRd == DestroyedRwlock); PthreadRwlock remoteWr = nullptr; Holder bgWriter{&remoteWr, true}; Start(bgWriter); - Require(pthread_rwlock_destroy_nid_postfix(&remoteWr) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&remoteWr) == SCE_KERNEL_ERROR_EBUSY); Require(remoteWr != nullptr); bgWriter.release.store(true); Require(scePthreadJoin(bgWriter.thread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&remoteWr) == 0); - Require(remoteWr == nullptr); + Require(remoteWr == DestroyedRwlock); CheckWriterHandoff(); CheckFailedLockAttempts(); + CheckDestroyLifecycle(); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 59ccac655..4ec893870 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -188,7 +188,7 @@ Throughout the project, every function at every stage either **does exactly what - [_umtx_op](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed -- [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a rwlock held for reading or writing returns `EBUSY` and keeps its handle, as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does; FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_rwlock.c) and shadPS4 free it without checking, and the console's answer is unverified. Destroying a rwlock while another thread is acquiring it is not guarded +- [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [sceKernelGetAvailableCpumask](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed From ecd4e90628d2a9a2706958cd21d3a2e4faed0756 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:51:28 -0400 Subject: [PATCH 142/318] fix(libc): preserve the guest ABI for array callbacks --- core/libs/prx/libc/CMakeLists.txt | 10 + .../prx/libc/src/exception/ArraySupport.cpp | 28 +-- core/libs/tests/GuestArrayCallbacks.cpp | 201 ++++++++++++++++++ 3 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 core/libs/tests/GuestArrayCallbacks.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d68768c32..ba65ae1e9 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -57,6 +57,16 @@ if(WIN32) endif() if(BUILD_TESTING) + add_test_executable(guest_array_callbacks_tests ${LIBS_INCLUDE_DIR}/tests/GuestArrayCallbacks.cpp) + target_include_directories(guest_array_callbacks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_array_callbacks_tests PRIVATE libc) + configure_windows_unwind(guest_array_callbacks_tests) + add_test(NAME guest_array_callbacks COMMAND guest_array_callbacks_tests) + set_tests_properties(guest_array_callbacks PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_array_callbacks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_time_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimeConversion.cpp) target_include_directories(guest_time_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_time_conversion_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/exception/ArraySupport.cpp b/core/libs/prx/libc/src/exception/ArraySupport.cpp index 8132ade49..6b5302ac9 100644 --- a/core/libs/prx/libc/src/exception/ArraySupport.cpp +++ b/core/libs/prx/libc/src/exception/ArraySupport.cpp @@ -2,13 +2,13 @@ #include extern "C" { -void APS5_VABI __cxa_vec_cleanup_nid_postfix(void* array, std::size_t count, std::size_t size, void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_cleanup_nid_postfix(void* array, std::size_t count, std::size_t size, void (APS5_VABI *destroy)(void*)) { if (!destroy) return; try { while (count) destroy(static_cast(array) + --count * size); } catch (...) { LibcException::Terminate(); } } -void APS5_VABI __cxa_vec_ctor_nid_postfix(void* array, std::size_t count, std::size_t size, void (*construct)(void*), void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_ctor_nid_postfix(void* array, std::size_t count, std::size_t size, void (APS5_VABI *construct)(void*), void (APS5_VABI *destroy)(void*)) { if (!construct) return; std::size_t complete = 0; try { @@ -18,7 +18,7 @@ void APS5_VABI __cxa_vec_ctor_nid_postfix(void* array, std::size_t count, std::s throw; } } -void APS5_VABI __cxa_vec_cctor_nid_postfix(void* destination, void* source, std::size_t count, std::size_t size, void (*copy)(void*, void*), void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_cctor_nid_postfix(void* destination, void* source, std::size_t count, std::size_t size, void (APS5_VABI *copy)(void*, void*), void (APS5_VABI *destroy)(void*)) { if (!copy) return; std::size_t complete = 0; try { @@ -29,7 +29,7 @@ void APS5_VABI __cxa_vec_cctor_nid_postfix(void* destination, void* source, std: throw; } } -void APS5_VABI __cxa_vec_dtor_nid_postfix(void* array, std::size_t count, std::size_t size, void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_dtor_nid_postfix(void* array, std::size_t count, std::size_t size, void (APS5_VABI *destroy)(void*)) { if (!destroy) return; bool unwinding = LibcException::globals.uncaught != 0; try { @@ -53,8 +53,8 @@ void* ArrayAllocate(std::size_t size) { if (!result) _ZSt11_Xbad_allocv_nid_postfix(); return result; } -template -void* NewArray(std::size_t count, std::size_t size, std::size_t padding, void (*construct)(void*), void (*destroy)(void*), void* (*allocate)(std::size_t), Deallocate deallocate) { +template +void* NewArray(std::size_t count, std::size_t size, std::size_t padding, void (APS5_VABI *construct)(void*), void (APS5_VABI *destroy)(void*), TAllocate allocate, TDeallocate deallocate) { std::size_t total = ArraySize(count, size, padding); auto* storage = static_cast(allocate(total)); if (!storage) return nullptr; @@ -64,8 +64,8 @@ void* NewArray(std::size_t count, std::size_t size, std::size_t padding, void (* catch (...) { deallocate(storage, total); throw; } return array; } -template -void DeleteArray(void* array, std::size_t size, std::size_t padding, void (*destroy)(void*), Deallocate deallocate) { +template +void DeleteArray(void* array, std::size_t size, std::size_t padding, void (APS5_VABI *destroy)(void*), TDeallocate deallocate) { if (!array) return; auto* storage = static_cast(array) - padding; std::size_t count = 0; @@ -81,22 +81,22 @@ void DeleteArray(void* array, std::size_t size, std::size_t padding, void (*dest } extern "C" { -void* APS5_VABI __cxa_vec_new_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (*ctor)(void*), void (*dtor)(void*)) { +void* APS5_VABI __cxa_vec_new_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (APS5_VABI *ctor)(void*), void (APS5_VABI *dtor)(void*)) { return LibcException::NewArray(n, s, p, ctor, dtor, LibcException::ArrayAllocate, [](void* ptr, std::size_t) { std::free(ptr); }); } -void* APS5_VABI __cxa_vec_new2_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (*ctor)(void*), void (*dtor)(void*), void* (*alloc)(std::size_t), void (*dealloc)(void*)) { +void* APS5_VABI __cxa_vec_new2_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (APS5_VABI *ctor)(void*), void (APS5_VABI *dtor)(void*), void* (APS5_VABI *alloc)(std::size_t), void (APS5_VABI *dealloc)(void*)) { return LibcException::NewArray(n, s, p, ctor, dtor, alloc, [dealloc](void* ptr, std::size_t) { dealloc(ptr); }); } -void* APS5_VABI __cxa_vec_new3_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (*ctor)(void*), void (*dtor)(void*), void* (*alloc)(std::size_t), void (*dealloc)(void*, std::size_t)) { +void* APS5_VABI __cxa_vec_new3_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (APS5_VABI *ctor)(void*), void (APS5_VABI *dtor)(void*), void* (APS5_VABI *alloc)(std::size_t), void (APS5_VABI *dealloc)(void*, std::size_t)) { return LibcException::NewArray(n, s, p, ctor, dtor, alloc, dealloc); } -void APS5_VABI __cxa_vec_delete_nid_postfix(void* array, std::size_t s, std::size_t p, void (*dtor)(void*)) { +void APS5_VABI __cxa_vec_delete_nid_postfix(void* array, std::size_t s, std::size_t p, void (APS5_VABI *dtor)(void*)) { LibcException::DeleteArray(array, s, p, dtor, [](void* ptr, std::size_t) { std::free(ptr); }); } -void APS5_VABI __cxa_vec_delete2_nid_postfix(void* array, std::size_t s, std::size_t p, void (*dtor)(void*), void (*dealloc)(void*)) { +void APS5_VABI __cxa_vec_delete2_nid_postfix(void* array, std::size_t s, std::size_t p, void (APS5_VABI *dtor)(void*), void (APS5_VABI *dealloc)(void*)) { LibcException::DeleteArray(array, s, p, dtor, [dealloc](void* ptr, std::size_t) { dealloc(ptr); }); } -void APS5_VABI __cxa_vec_delete3_nid_postfix(void* array, std::size_t s, std::size_t p, void (*dtor)(void*), void (*dealloc)(void*, std::size_t)) { +void APS5_VABI __cxa_vec_delete3_nid_postfix(void* array, std::size_t s, std::size_t p, void (APS5_VABI *dtor)(void*), void (APS5_VABI *dealloc)(void*, std::size_t)) { LibcException::DeleteArray(array, s, p, dtor, dealloc); } } diff --git a/core/libs/tests/GuestArrayCallbacks.cpp b/core/libs/tests/GuestArrayCallbacks.cpp new file mode 100644 index 000000000..92bfdfdef --- /dev/null +++ b/core/libs/tests/GuestArrayCallbacks.cpp @@ -0,0 +1,201 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +using ElementCallback = void (APS5_VABI *)(void*); +using CopyCallback = void (APS5_VABI *)(void*, void*); +using AllocateCallback = void* (APS5_VABI *)(std::size_t); +using FreeCallback = void (APS5_VABI *)(void*); +using SizedFreeCallback = void (APS5_VABI *)(void*, std::size_t); + +extern "C" { +void APS5_VABI __cxa_vec_ctor_nid_postfix(void*, std::size_t, std::size_t, ElementCallback, ElementCallback); +void APS5_VABI __cxa_vec_cctor_nid_postfix(void*, void*, std::size_t, std::size_t, CopyCallback, ElementCallback); +void APS5_VABI __cxa_vec_dtor_nid_postfix(void*, std::size_t, std::size_t, ElementCallback); +void APS5_VABI __cxa_vec_cleanup_nid_postfix(void*, std::size_t, std::size_t, ElementCallback); +void* APS5_VABI __cxa_vec_new_nid_postfix(std::size_t, std::size_t, std::size_t, ElementCallback, ElementCallback); +void* APS5_VABI __cxa_vec_new2_nid_postfix(std::size_t, std::size_t, std::size_t, ElementCallback, ElementCallback, AllocateCallback, FreeCallback); +void* APS5_VABI __cxa_vec_new3_nid_postfix(std::size_t, std::size_t, std::size_t, ElementCallback, ElementCallback, AllocateCallback, SizedFreeCallback); +void APS5_VABI __cxa_vec_delete_nid_postfix(void*, std::size_t, std::size_t, ElementCallback); +void APS5_VABI __cxa_vec_delete2_nid_postfix(void*, std::size_t, std::size_t, ElementCallback, FreeCallback); +void APS5_VABI __cxa_vec_delete3_nid_postfix(void*, std::size_t, std::size_t, ElementCallback, SizedFreeCallback); +} + +namespace { + +constexpr std::size_t Count = 4; +constexpr std::size_t ElementBytes = 16; +constexpr std::size_t Padding = 16; +constexpr std::size_t TotalBytes = Count * ElementBytes + Padding; +alignas(16) std::array storage{}; +alignas(16) std::array source{}; +std::byte* destination = nullptr; +std::size_t constructed = 0; +std::size_t copied = 0; +std::size_t destroyed = 0; +std::size_t allocated = 0; +std::size_t freed = 0; +bool failAllocation = false; +bool failConstruct = false; +bool failCopy = false; +bool failDestroy = false; + +void require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +void reset(std::byte* first) { + destination = first; + constructed = copied = destroyed = allocated = freed = 0; + failAllocation = false; + failConstruct = failCopy = failDestroy = false; +} + +void APS5_VABI construct(void* pointer) { + require(constructed < Count && pointer == destination + constructed * ElementBytes, "constructor received the wrong element"); + if (failConstruct && constructed == 2) throw 73; + std::memset(pointer, static_cast(++constructed), ElementBytes); +} + +void APS5_VABI copy(void* target, void* original) { + require(copied < Count && target == destination + copied * ElementBytes, "copy constructor received the wrong destination"); + require(original == source.data() + copied * ElementBytes, "copy constructor received the wrong source"); + if (failCopy && copied == 2) throw 73; + std::memcpy(target, original, ElementBytes); + ++copied; +} + +void APS5_VABI destroy(void* pointer) { + const auto count = copied != 0 ? copied : constructed; + require(destroyed < count && pointer == destination + (count - destroyed - 1) * ElementBytes, "destructor received the wrong element or order"); + require(*static_cast(pointer) == std::byte(count - destroyed), "destructor received the wrong element contents"); + ++destroyed; + if (failDestroy && destroyed == 1) throw 73; +} + +void* APS5_VABI allocate(std::size_t bytes) { + require(bytes == TotalBytes, "allocator received the wrong size"); + ++allocated; + return failAllocation ? nullptr : storage.data(); +} + +void APS5_VABI release(void* pointer) { + require(pointer == storage.data(), "deallocator received the wrong storage"); + require(destroyed == constructed, "deallocator ran before element destruction finished"); + ++freed; +} + +void APS5_VABI releaseSized(void* pointer, std::size_t bytes) { + require(bytes == TotalBytes, "sized deallocator received the wrong size"); + release(pointer); +} + +void checkCookie(const void* array) { + std::size_t count = 0; + std::memcpy(&count, static_cast(array) - sizeof(count), sizeof(count)); + require(count == Count, "array cookie has the wrong count"); +} + +void checkElements() { + reset(source.data()); + __cxa_vec_ctor_nid_postfix(source.data(), Count, ElementBytes, construct, destroy); + require(constructed == Count, "constructor count is wrong"); + __cxa_vec_dtor_nid_postfix(source.data(), Count, ElementBytes, destroy); + require(destroyed == Count, "destructor count is wrong"); + reset(storage.data()); + __cxa_vec_cctor_nid_postfix(storage.data(), source.data(), Count, ElementBytes, copy, destroy); + require(copied == Count && std::memcmp(storage.data(), source.data(), source.size()) == 0, "copy construction failed"); + __cxa_vec_cleanup_nid_postfix(storage.data(), Count, ElementBytes, destroy); + require(destroyed == Count, "cleanup count is wrong"); + __cxa_vec_ctor_nid_postfix(nullptr, Count, ElementBytes, nullptr, nullptr); + __cxa_vec_cctor_nid_postfix(nullptr, nullptr, Count, ElementBytes, nullptr, nullptr); + __cxa_vec_dtor_nid_postfix(nullptr, Count, ElementBytes, nullptr); + __cxa_vec_cleanup_nid_postfix(nullptr, Count, ElementBytes, nullptr); +} + +void checkAllocation(bool sized) { + reset(storage.data() + Padding); + void* array = sized + ? __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized) + : __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + require(array == destination && constructed == Count && allocated == 1, "array allocation failed"); + checkCookie(array); + if (sized) __cxa_vec_delete3_nid_postfix(array, ElementBytes, Padding, destroy, releaseSized); + else __cxa_vec_delete2_nid_postfix(array, ElementBytes, Padding, destroy, release); + require(destroyed == Count && freed == 1, "array deletion failed"); + reset(storage.data() + Padding); + failAllocation = true; + array = sized + ? __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized) + : __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + require(array == nullptr && allocated == 1 && constructed == 0 && freed == 0, "failed allocation invoked element callbacks"); + if (sized) __cxa_vec_delete3_nid_postfix(nullptr, ElementBytes, Padding, destroy, releaseSized); + else __cxa_vec_delete2_nid_postfix(nullptr, ElementBytes, Padding, destroy, release); + require(freed == 0, "null array invoked deallocation"); +} + +void checkDefaultAllocation() { + void* array = __cxa_vec_new_nid_postfix(Count, ElementBytes, Padding, nullptr, nullptr); + require(array != nullptr, "default array allocation failed"); + checkCookie(array); + reset(static_cast(array)); + __cxa_vec_ctor_nid_postfix(array, Count, ElementBytes, construct, destroy); + __cxa_vec_delete_nid_postfix(array, ElementBytes, Padding, destroy); + require(constructed == Count && destroyed == Count, "default array deletion failed"); + __cxa_vec_delete_nid_postfix(nullptr, ElementBytes, Padding, destroy); +} + +template +void expectException(TAction action) { + bool caught = false; + try { action(); } + catch (int value) { require(value == 73, "callback exception changed"); caught = true; } + require(caught, "callback exception was swallowed"); +} + +void checkRollback(bool sized) { + reset(storage.data() + Padding); + failConstruct = true; + expectException([&] { + if (sized) __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized); + else __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + }); + require(constructed == 2 && destroyed == 2 && allocated == 1 && freed == 1, "constructor rollback failed"); + reset(storage.data() + Padding); + void* array = sized + ? __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized) + : __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + failDestroy = true; + expectException([&] { + if (sized) __cxa_vec_delete3_nid_postfix(array, ElementBytes, Padding, destroy, releaseSized); + else __cxa_vec_delete2_nid_postfix(array, ElementBytes, Padding, destroy, release); + }); + require(constructed == Count && destroyed == Count && allocated == 1 && freed == 1, "destructor rollback failed"); +} + +void checkCopyRollback() { + reset(storage.data()); + failCopy = true; + expectException([] { + __cxa_vec_cctor_nid_postfix(storage.data(), source.data(), Count, ElementBytes, copy, destroy); + }); + require(copied == 2 && destroyed == 2, "copy constructor rollback failed"); +} + +} + +int main() { + checkElements(); + checkAllocation(false); + checkAllocation(true); + checkDefaultAllocation(); + checkRollback(false); + checkRollback(true); + checkCopyRollback(); + std::puts("guest array callback arguments and lifecycle: PASS"); +} From de54527a8b239d505723ecf1028b8e47877974b8 Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:31:45 -0400 Subject: [PATCH 143/318] fix(shader): keep f32 denormals in compares when denormals are kept With the f32 denormal field of FLOAT_MODE set to keep, v_cmp_X_f32 and v_cmpx_X_f32 read a denormal operand as zero: NVIDIA, RADV and lavapipe all flush denormals in their float compares. Compare the operands as sign-magnitude integers instead, with NaN handled apart. Compares with flushed denormals are unchanged. This matches RDNA2 (gfx1036) on 256 operand pairs per opcode, with f32 denormals kept and flushed. Co-Authored-By: Claude Sonnet 5.5 --- .../tests/execution/VCmpF32Denormals.cpp | 173 ++++++++++++++++++ .../Translation/TranslationContext.hpp | 1 + .../Translation/src/CompareInstructions.cpp | 41 +++++ docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp new file mode 100644 index 000000000..54e132b6c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp @@ -0,0 +1,173 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t ValueCount = 16; +constexpr std::uint32_t Groups = ValueCount * ValueCount / Threads; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +constexpr std::uint32_t Values[ValueCount] = { + 0x00000000u, 0x80000000u, 0x00000001u, 0x80000001u, 0x007fffffu, 0x807fffffu, 0x00800000u, 0x80800000u, + 0x3f800000u, 0xbf800000u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0x7f800001u, 0xffc00000u, 0x7f7fffffu, +}; + +constexpr std::uint32_t CaptureExec = 0xbe95037eu; +constexpr std::uint32_t CaptureVcc = 0xbe95036au; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbe94037eu, 0x7c240b04u, + 0xbe95037eu, 0xbefe0314u, 0xd501000au, 0x00550280u, 0xe0701000u, 0x80010a03u, 0xbf810000u, +}; + +struct Row { + const char* condition; + std::uint32_t cmpx; + std::uint32_t cmp; + std::uint32_t kept[Groups]; + std::uint32_t flushed[Groups]; +}; + +constexpr std::uint32_t RowCount = 12; +constexpr Row Rows[RowCount] = { + {"eq", 0x7c240b04u, 0x7c040b04u, + {0x00030003u, 0x00080004u, 0x00200010u, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}, + {0x003f003fu, 0x003f003fu, 0x003f003fu, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}}, + {"ge", 0x7c2c0b04u, 0x7c0c0b04u, + {0x0aab0aabu, 0x0aa80aafu, 0x0aa00abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}, + {0x0abf0abfu, 0x0abf0abfu, 0x0abf0abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}}, + {"gt", 0x7c280b04u, 0x7c080b04u, + {0x0aa80aa8u, 0x0aa00aabu, 0x0a800aafu, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}, + {0x0a800a80u, 0x0a800a80u, 0x0a800a80u, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}}, + {"le", 0x7c260b04u, 0x7c060b04u, + {0x85578557u, 0x855f8554u, 0x857f8550u, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}, + {0x857f857fu, 0x857f857fu, 0x857f857fu, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}}, + {"lg", 0x7c2a0b04u, 0x7c0a0b04u, + {0x8ffc8ffcu, 0x8ff78ffbu, 0x8fdf8fefu, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}, + {0x8fc08fc0u, 0x8fc08fc0u, 0x8fc08fc0u, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}}, + {"lt", 0x7c220b04u, 0x7c020b04u, + {0x85548554u, 0x85578550u, 0x855f8540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}, + {0x85408540u, 0x85408540u, 0x85408540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}}, + {"neq", 0x7c3a0b04u, 0x7c1a0b04u, + {0xfffcfffcu, 0xfff7fffbu, 0xffdfffefu, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}, + {0xffc0ffc0u, 0xffc0ffc0u, 0xffc0ffc0u, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}}, + {"nge", 0x7c320b04u, 0x7c120b04u, + {0xf554f554u, 0xf557f550u, 0xf55ff540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}, + {0xf540f540u, 0xf540f540u, 0xf540f540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}}, + {"ngt", 0x7c360b04u, 0x7c160b04u, + {0xf557f557u, 0xf55ff554u, 0xf57ff550u, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}, + {0xf57ff57fu, 0xf57ff57fu, 0xf57ff57fu, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}}, + {"nle", 0x7c380b04u, 0x7c180b04u, + {0x7aa87aa8u, 0x7aa07aabu, 0x7a807aafu, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}, + {0x7a807a80u, 0x7a807a80u, 0x7a807a80u, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}}, + {"nlg", 0x7c340b04u, 0x7c140b04u, + {0x70037003u, 0x70087004u, 0x70207010u, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}, + {0x703f703fu, 0x703f703fu, 0x703f703fu, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}}, + {"nlt", 0x7c3c0b04u, 0x7c1c0b04u, + {0x7aab7aabu, 0x7aa87aafu, 0x7aa07abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}, + {0x7abf7abfu, 0x7abf7abfu, 0x7abf7abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture, std::uint32_t group) { + Code[6] = compare; + Code[7] = capture; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const std::uint32_t pair = group * Threads + lane; + Input[lane * Inputs] = Values[pair / ValueCount]; + Input[lane * Inputs + 1u] = Values[pair % ValueCount]; + Input[lane * Inputs + 2u] = 0u; + Input[lane * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + std::uint32_t mask = 0u; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] <= 1u, "v_cmp f32 denormals: lane " + std::to_string(lane) + " stored " + Hex(Output[lane])); + mask |= Output[lane] << lane; + } + return mask; +} + +void Check(AgcDriver::VulkanDevice& device, const char* modeName, ShaderRecompiler::ShaderFloatMode mode, const Row& row, bool exec, const std::uint32_t* expected) { + const std::string name = std::string(exec ? "v_cmpx_" : "v_cmp_") + row.condition + "_f32"; + for (std::uint32_t group = 0; group < Groups; ++group) { + const std::uint32_t actual = Run(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc, group); + if (actual == expected[group]) continue; + const std::uint32_t diff = actual ^ expected[group]; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + if ((diff >> lane & 1u) == 0u) continue; + const std::uint32_t pair = group * Threads + lane; + Require(false, + name + " " + modeName + " of " + Hex(Values[pair / ValueCount]) + " and " + + Hex(Values[pair % ValueCount]) + " is " + std::to_string(actual >> lane & 1u) + ", expected " + + std::to_string(expected[group] >> lane & 1u)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const Row& row : Rows) { + for (const bool exec : {true, false}) { + Check(*device, "denormals kept, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, row, exec, row.kept); + Check(*device, "denormals kept, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, false, false, false}, row, exec, row.kept); + Check(*device, "denormals flushed, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}, row, exec, row.flushed); + Check(*device, "denormals flushed, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}, row, exec, row.flushed); + } + } + std::puts("v_cmp and v_cmpx f32 denormals tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 7a9d6fc8f..ca2f57180 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -161,6 +161,7 @@ private: void emitInteger16Compare(const RdnaInstruction& inst, IrOpcode opcode, bool signedValue, bool cmpx); void emitFloatCompare(const RdnaInstruction& inst, IrOpcode opcode, bool half, bool cmpx, bool swap = false); IrU1 floatCompareWithConstant(IrOpcode opcode, IrU32 variableBits, std::uint32_t constant); + IrU1 floatCompareKeepingDenormals(IrOpcode opcode, IrU32 lhsBits, IrU32 rhsBits); void emitInteger64Order(const RdnaInstruction& inst, bool signedValue, bool swap, bool negate, bool cmpx); void emitFloatOrderedCompare(const RdnaInstruction& inst, bool ordered, bool half, bool cmpx); void emitFloatClassCompare(const RdnaInstruction& inst, bool cmpx); diff --git a/core/shader/recompiler/Translation/src/CompareInstructions.cpp b/core/shader/recompiler/Translation/src/CompareInstructions.cpp index eac2f2a94..50bdf556c 100644 --- a/core/shader/recompiler/Translation/src/CompareInstructions.cpp +++ b/core/shader/recompiler/Translation/src/CompareInstructions.cpp @@ -50,6 +50,29 @@ bool unorderedInfinityCompareIsTrue(IrOpcode opcode, bool constantIsLhs, std::ui return false; } +struct OrderedRelation { + IrOpcode integerOpcode; + bool unordered; +}; + +OrderedRelation orderedRelationOf(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::FPOrdEqual32: return {IrOpcode::IEqual32, false}; + case IrOpcode::FPUnordEqual32: return {IrOpcode::IEqual32, true}; + case IrOpcode::FPOrdNotEqual32: return {IrOpcode::INotEqual32, false}; + case IrOpcode::FPUnordNotEqual32: return {IrOpcode::INotEqual32, true}; + case IrOpcode::FPOrdLessThan32: return {IrOpcode::SLessThan32, false}; + case IrOpcode::FPUnordLessThan32: return {IrOpcode::SLessThan32, true}; + case IrOpcode::FPOrdLessThanEqual32: return {IrOpcode::SLessThanEqual32, false}; + case IrOpcode::FPUnordLessThanEqual32: return {IrOpcode::SLessThanEqual32, true}; + case IrOpcode::FPOrdGreaterThan32: return {IrOpcode::SGreaterThan32, false}; + case IrOpcode::FPUnordGreaterThan32: return {IrOpcode::SGreaterThan32, true}; + case IrOpcode::FPOrdGreaterThanEqual32: return {IrOpcode::SGreaterThanEqual32, false}; + case IrOpcode::FPUnordGreaterThanEqual32: return {IrOpcode::SGreaterThanEqual32, true}; + default: throw std::runtime_error("f32 compare keeping denormals: unexpected compare opcode"); + } +} + } void TranslateCompareInstruction(IrBuilder& builder, const RdnaInstruction& instruction) { @@ -113,6 +136,10 @@ void TranslationContext::emitFloatCompare(const RdnaInstruction& inst, IrOpcode emitCompareResult(inst, floatCompareWithConstant(oriented, IrU32(lhsConstant.has_value() ? *rhsBits : *lhsBits), lhsConstant.has_value() ? *lhsConstant : *rhsConstant), false, cmpx); return; } + if ((f32DenormalFlush & 1u) == 0u) { + emitCompareResult(inst, floatCompareKeepingDenormals(opcode, IrU32(*lhsBits), IrU32(*rhsBits)), false, cmpx); + return; + } lhs = &ir.BitCastF32(flushF32Denormal(IrU32(*lhsBits)).Value()); rhs = &ir.BitCastF32(flushF32Denormal(IrU32(*rhsBits)).Value()); emitCompareResult(inst, IrU1(ir.Emit(opcode, IrType::U1, {lhs, rhs})), false, cmpx); @@ -122,6 +149,20 @@ void TranslationContext::emitFloatCompare(const RdnaInstruction& inst, IrOpcode emitCompareResult(inst, IrU1(ir.Emit(opcode, IrType::U1, {lhs, rhs})), false, cmpx); } +IrU1 TranslationContext::floatCompareKeepingDenormals(IrOpcode opcode, IrU32 lhsBits, IrU32 rhsBits) { + const auto magnitude = [&](IrU32 bits) -> IrValue& { return ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)); }; + const auto order = [&](IrU32 bits) -> IrValue& { + IrValue& absolute = magnitude(bits); + IrValue& negative = ir.INotEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), ir.Constant(0u)); + return ir.Select(negative, ir.ISub(ir.Constant(0u), absolute), absolute); + }; + const auto isNan = [&](IrU32 bits) -> IrValue& { return ir.UGreaterThan(magnitude(bits), ir.Constant(0x7f800000u)); }; + const OrderedRelation relation = orderedRelationOf(opcode); + IrValue& ordering = ir.Emit(relation.integerOpcode, IrType::U1, {&order(lhsBits), &order(rhsBits)}); + IrValue& eitherNan = ir.LogicalOr(isNan(lhsBits), isNan(rhsBits)); + return IrU1(relation.unordered ? ir.LogicalOr(eitherNan, ordering) : ir.LogicalAnd(ir.LogicalNot(eitherNan), ordering)); +} + IrU1 TranslationContext::floatCompareWithConstant(IrOpcode opcode, IrU32 variableBits, std::uint32_t constant) { IrValue& value = ir.BitCastF32(variableBits.Value()); const auto compare = [&](IrOpcode compareOpcode, IrValue& lhs, std::uint32_t rhs) { return IrU1(ir.Emit(compareOpcode, IrType::U1, {&lhs, &ir.BitCastF32(ir.Constant(rhs))})); }; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..e236a4168 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -520,7 +520,7 @@ Throughout the project, every function at every stage either **does exactly what - [Signaling NaN inputs](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [Packed f16 NaN results](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. -- [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` is the exception: it preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. +- [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. - [Overlay output bus](../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. From 726e773417b9608b392dcc4f824690b0bb772879 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:42:39 +0530 Subject: [PATCH 144/318] feat(libkernel): support kern.sdk_version in sysctlbyname --- .../libkernel/System/src/SystemConfiguration.cpp | 8 ++++++++ core/libs/tests/GuestSystemConfiguration.cpp | 16 ++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 25 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 106e28393..a9c9b7ce1 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -157,6 +157,14 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); return 0; } + if (std::strcmp(name, "kern.sdk_version") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static constexpr std::uint32_t sdkVersion = 0x01000000; + return CopyOut(&sdkVersion, sizeof(sdkVersion), oldValue, oldLength); + } if (std::strcmp(name, "hw.ncpu") == 0) { static constexpr int mib[] = {6, 3}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index e29adaa60..6ba24d971 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -179,6 +179,21 @@ static void CheckPseudoRandomSysctl() { } Require(threw); } +static void CheckSdkVersionSysctl() { + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctlbyname_nid_postfix("kern.sdk_version", nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint32_t)); + std::uint32_t version = 0; + length = sizeof(version); + Require(sysctlbyname_nid_postfix("kern.sdk_version", &version, &length, nullptr, 0) == 0 && version == 0x01000000 && length == sizeof(std::uint32_t)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("kern.sdk_version", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && bytes[2] == 0xaa && bytes[3] == 0xaa); + length = sizeof(version); + Require(sysctlbyname_nid_postfix("kern.sdk_version", &version, &length, &version, sizeof(version)) == -1 && *__error_nid_postfix() == 1); +} int main() { CheckProcessorCountSysctl(); CheckPhysicalMemorySysctl(); @@ -186,6 +201,7 @@ int main() { CheckPathnameSysctl(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); + CheckSdkVersionSysctl(); *__error_nid_postfix() = 13; Require(sysconf_nid_postfix(47) == 0x4000); Require(getpagesize_nid_postfix() == sysconf_nid_postfix(47)); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..ac807a7f5 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -56,6 +56,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNpGetSandboxInfo](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [sceKernelGetSystemSwVersion](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000). The 0x28-byte layout (`uint64_t size` at 0, `versionString` at 8 and `version` at 0x24) matches [ProsperoStore's caller](https://github.com/blackbearreloaded/ProsperoStore/blob/20584011/src/diag/trace.cpp#L20-L26), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d1/src/core/libraries/kernel/kernel.h#L45-L49), [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6/kernel/ps4_libkernel.pas#L502-L506) and [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4/include/orbis/_types/kernel.h#L256-L260). These sources support the structure layout, but not the reported version value: the console firmware version remains unverified. The checks are unverified too: a null pointer returns `SCE_KERNEL_ERROR_EFAULT` and a `size` other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL`, while shadPS4 returns success for null and never reads `size`. - [kern.rng_pseudo](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown +- [kern.sdk_version](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - PPSA01530, PPSA23827 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), and [scePadUnknown_n3kSX62fgNo](../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. - [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. - [sceHttp2WebSocketSetPingInterval](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. From 0245ba7d883db3fc21e71e1111512bf2c00bf5e0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:25:00 -0700 Subject: [PATCH 145/318] fix(libc): open fopen streams with delete sharing on Windows fopen opened its stream with _wfopen, which shares read and write access but not delete access. While the guest held a stream open, rename of its file failed with EIO and remove with EBUSY. POSIX rename and remove work on open files, as on Linux and FreeBSD, and since #3677 the descriptors sceKernelOpen returns share delete access. fopen now creates the handle with CreateFileW sharing read, write and delete, with the access and creation disposition of the mode, and wraps it with _open_osfhandle and _wfdopen, keeping append mode. A failed CreateFileW sets errno through the error mapping #3677 added to libkernel's NativeOpen, which moves to libc as WindowsOpenError_nid_no_patch so that both use it. freopen still reopens through _wfreopen; TechnicalDebt records it. --- core/libs/prx/libc/include/General.hpp | 3 ++ core/libs/prx/libc/src/FileAndHeap.cpp | 34 ++++++++++++++++++++-- core/libs/prx/libc/src/General.cpp | 35 +++++++++++++++++++++++ core/libs/prx/libkernel/File/src/Open.cpp | 34 +--------------------- core/libs/tests/FileStreams.cpp | 23 +++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 94 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 2a2a9ed92..7de9ed967 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -24,6 +24,9 @@ extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path extern "C" std::vector WrittenPaths_nid_no_patch(); extern "C" void SyncWrittenPaths_nid_no_patch(); extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix); +#ifdef _WIN32 +extern "C" int WindowsOpenError_nid_no_patch(unsigned long error); +#endif #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 87c352e2b..f4548f666 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -11,6 +11,14 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#endif #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/ApplicationHeap.hpp" @@ -45,9 +53,29 @@ static std::optional ParseFileMode(const char* mode) { static std::FILE* OpenNative(const std::filesystem::path& path, const std::string& nativeMode) { #ifdef _WIN32 - std::wstring wideMode; - for (const char character : nativeMode) wideMode.push_back(static_cast(character)); - return ::_wfopen(path.c_str(), wideMode.c_str()); + const char access = nativeMode[0]; + const bool update = nativeMode.find('+') != std::string::npos; + const DWORD desired = update ? GENERIC_READ | GENERIC_WRITE : access == 'r' ? GENERIC_READ : GENERIC_WRITE; + const DWORD disposition = access == 'r' ? OPEN_EXISTING + : nativeMode.find('x') != std::string::npos ? CREATE_NEW : access == 'w' ? CREATE_ALWAYS : OPEN_ALWAYS; + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + const auto handle = CreateFileW(path.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, + disposition, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return nullptr; + } + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), access == 'a' ? _O_APPEND : 0); + if (descriptor < 0) { + CloseHandle(handle); + return nullptr; + } + std::wstring streamMode; + for (const char character : nativeMode) + if (character != 'x') streamMode.push_back(static_cast(character)); + std::FILE* stream = ::_wfdopen(descriptor, streamMode.c_str()); + if (!stream) ::_close(descriptor); + return stream; #else return std::fopen(path.c_str(), nativeMode.c_str()); #endif diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index fefc33af4..a2a57370b 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -401,6 +401,41 @@ extern "C" char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size) { catch (const std::filesystem::filesystem_error& error) { errno = DirectoryFailure(error.code()); return nullptr; } } +#ifdef _WIN32 +extern "C" int WindowsOpenError_nid_no_patch(unsigned long error) { + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_DRIVE: + case ERROR_BAD_NETPATH: + case ERROR_BAD_NET_NAME: + case ERROR_BAD_PATHNAME: + case ERROR_FILENAME_EXCED_RANGE: + return ENOENT; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + case ERROR_WRITE_PROTECT: + case ERROR_NETWORK_ACCESS_DENIED: + case ERROR_CANNOT_MAKE: + case ERROR_DRIVE_LOCKED: + return EACCES; + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: + return EEXIST; + case ERROR_TOO_MANY_OPEN_FILES: + return EMFILE; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return ENOMEM; + case ERROR_DISK_FULL: + return ENOSPC; + default: + return EINVAL; + } +} +#endif + extern "C" void NotImplemented_nid_no_patch(const char* funcName) { throw std::runtime_error(std::string(funcName) + " not implemented"); } diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index bfcf004c5..58e3c3c70 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -23,38 +23,6 @@ #include #include #include -static int OpenError(DWORD error) { - switch (error) { - case ERROR_FILE_NOT_FOUND: - case ERROR_PATH_NOT_FOUND: - case ERROR_INVALID_DRIVE: - case ERROR_BAD_NETPATH: - case ERROR_BAD_NET_NAME: - case ERROR_BAD_PATHNAME: - case ERROR_FILENAME_EXCED_RANGE: - return ENOENT; - case ERROR_ACCESS_DENIED: - case ERROR_SHARING_VIOLATION: - case ERROR_LOCK_VIOLATION: - case ERROR_WRITE_PROTECT: - case ERROR_NETWORK_ACCESS_DENIED: - case ERROR_CANNOT_MAKE: - case ERROR_DRIVE_LOCKED: - return EACCES; - case ERROR_FILE_EXISTS: - case ERROR_ALREADY_EXISTS: - return EEXIST; - case ERROR_TOO_MANY_OPEN_FILES: - return EMFILE; - case ERROR_NOT_ENOUGH_MEMORY: - case ERROR_OUTOFMEMORY: - return ENOMEM; - case ERROR_DISK_FULL: - return ENOSPC; - default: - return EINVAL; - } -} static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR); const DWORD desired = access == _O_RDONLY ? GENERIC_READ : access == _O_WRONLY ? GENERIC_WRITE : GENERIC_READ | GENERIC_WRITE; @@ -65,7 +33,7 @@ static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; const auto handle = CreateFileW(p.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, attributes, nullptr); if (handle == INVALID_HANDLE_VALUE) { - errno = OpenError(GetLastError()); + errno = WindowsOpenError_nid_no_patch(GetLastError()); return -1; } const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), nativeFlags & (_O_APPEND | _O_RDONLY | _O_BINARY)); diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index aa4b41128..c06b0a06a 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -19,6 +19,8 @@ int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int ori std::int64_t APS5_VABI ftell_nid_postfix(FileStream* stream); int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream); int APS5_VABI fflush_nid_postfix(FileStream* stream); +int APS5_VABI remove_nid_postfix(const char* path); +int APS5_VABI rename_nid_postfix(const char* from, const char* to); } static void Require(bool condition) { @@ -91,5 +93,26 @@ int main(int argc, char** argv) { Require(fclose_nid_postfix(stream) == 0); Require(std::filesystem::file_size(hostName) == 0); std::filesystem::remove(hostName); + const std::string heldName = std::string(argv[1]) + ".held"; + const std::string movedName = std::string(argv[1]) + ".moved"; + stream = fopen_nid_postfix(heldName.c_str(), "w+b"); + Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload)); + Require(fflush_nid_postfix(stream) == 0); + Require(rename_nid_postfix(heldName.c_str(), movedName.c_str()) == 0 && !std::filesystem::exists(heldName)); + Require(remove_nid_postfix(movedName.c_str()) == 0 && !std::filesystem::exists(movedName)); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0); + std::array heldBuffer{}; + Require(fread_nid_postfix(heldBuffer.data(), 1, heldBuffer.size(), stream) == heldBuffer.size()); + Require(std::strcmp(heldBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "ab"); + Require(stream != nullptr && fputs_nid_postfix("ab", stream) >= 0 && fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "ab"); + Require(stream != nullptr && fputs_nid_postfix("cd", stream) >= 0 && fclose_nid_postfix(stream) == 0); + errno = 0; + Require(fopen_nid_postfix(heldName.c_str(), "wbx") == nullptr && errno == EEXIST); + stream = fopen_nid_postfix(heldName.c_str(), "rb"); + std::array appended{}; + Require(stream != nullptr && fread_nid_postfix(appended.data(), 1, 4, stream) == 4 && std::strcmp(appended.data(), "abcd") == 0); + Require(fclose_nid_postfix(stream) == 0 && remove_nid_postfix(heldName.c_str()) == 0); std::cout << "PASS: stream objects, file operations, EOF and error handling\n"; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..158208540 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -431,6 +431,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way +- [`fopen` streams](../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access - [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`) - [makecontext](../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) follows FreeBSD 11's `lib/libc/amd64/gen/makecontext.c`: at most six arguments, a stack of at least `MINSIGSTKSZ` (2048) bytes, and `exit(0)` when the function returns with no `uc_link`. The console's version is not verified - [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 f2d698674189481e3ce91bbaed7be381acef81a4 Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:57:19 -0300 Subject: [PATCH 146/318] feat(agc): select storage dumps by shape and dump every layer APS5_DUMP_STORAGE also takes xx[:], for images whose address changes from run to run (a title's 32x32x32 color-grading LUT), and the dump holds the first mip of every layer or 3D slice instead of the first one only. --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2753e91fc..ea74a5883 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2976,6 +2976,37 @@ bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes return PublishShadow(address, bytes, scope, PublishReasonFor(reason)) != 0; } +namespace { + +struct StorageDumpSelector { + std::uint64_t address = 0; + std::uint32_t width = 0, height = 0, layers = 0, format = 0; + bool anyFormat = true; +}; + +const StorageDumpSelector& StorageDumpSelection() { + static const StorageDumpSelector selector = [] { + StorageDumpSelector parsed; + const char* text = std::getenv("APS5_DUMP_STORAGE"); + if (text == nullptr) return parsed; + if (std::sscanf(text, "%ux%ux%u:%u", &parsed.width, &parsed.height, &parsed.layers, &parsed.format) >= 3) { + parsed.anyFormat = std::strchr(text, ':') == nullptr; + return parsed; + } + parsed.address = std::strtoull(text, nullptr, 16); + return parsed; + }(); + return selector; +} + +bool StorageDumpSelects(const GuestTextureResource& descriptor) { + const auto& selector = StorageDumpSelection(); + if (selector.address != 0) return descriptor.baseAddress == selector.address; + return selector.width != 0 && descriptor.width == selector.width && descriptor.height == selector.height && descriptor.depthOrLastArray + 1 == selector.layers && (selector.anyFormat || descriptor.format == selector.format); +} + +} + void StorageTexture::FlushAllPending(const char* reason) { static_cast(FlushPending(0, std::numeric_limits::max(), nullptr, reason)); } @@ -4257,15 +4288,15 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); } } - // Debug aid: APS5_DUMP_STORAGE= saves that storage image's first mip after each of - // its first 8 write-backs as storage_
_.raw (u32 width, height, VkFormat, then rows). - static const std::uint64_t dumpAddress = [] { const char* text = std::getenv("APS5_DUMP_STORAGE"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); + const bool dumpSelected = StorageDumpSelects(descriptor); static int dumps = 0; std::unique_ptr dump; - if (dumpAddress != 0 && descriptor.baseAddress == dumpAddress && dumps < 8) { - dump = std::make_unique(context, static_cast(mips[0].linearSize), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + if (dumpSelected && dumps < 8) { + dump = std::make_unique(context, static_cast(mips[0].linearSize * arrayLayers), VK_BUFFER_USAGE_TRANSFER_DST_BIT); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); - CopyBuffer(context, commands, linear.Handle(), mips[0].linearOffset, dump->Handle(), 0, mips[0].linearSize); + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (geometry.HasLayer(0, layer)) CopyBuffer(context, commands, linear.Handle(), geometry.LinearLayerOffset(layer) + mips[0].linearOffset, dump->Handle(), layer * mips[0].linearSize, mips[0].linearSize); + } } const auto toCopy = WholeBufferBarrier(tiled.Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); VkImageMemoryBarrier backToGeneral = toSource; From a70ec3946d9bcfaace0c1b3566f978d801e41af8 Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:57:39 -0300 Subject: [PATCH 147/318] feat(agc): store the dumped storage images at each flip An image whose results stay on the GPU is never written back, so APS5_DUMP_STORAGE saw nothing; APS5_STORE_AT_FLIP=1 stores every pending image and ran a 4 GB device out of memory at 3840x2160. The selected images alone are now stored at each flip. --- .../libSceAgcDriver/Execution/src/VulkanDevice.cpp | 1 + .../libSceAgcDriver/Graphics/include/Texture.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Texture.cpp | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 8a1600be3..6b269c4d8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2469,6 +2469,7 @@ FrameDumps& Dumps() { bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP")); if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip"); + else Graphics::StorageTexture::FlushDumpSelected("flip"); if (buffer.tilingMode == 1) { const auto pixels = ReadDisplayBuffer(buffer); return present(buffer.width, buffer.height, true, pixels); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index a2d84f038..a01cd7efc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -178,6 +178,7 @@ public: static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr); static void FlushAllPending(const char* reason); static bool AnyPending(); + static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); // See PendingSerial: a change of a surface's source outside the registry (a unit shadow // retile) moves it too. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ea74a5883..92f24c687 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3007,6 +3007,20 @@ bool StorageDumpSelects(const GuestTextureResource& descriptor) { } +void StorageTexture::FlushDumpSelected(const char* reason) { + const auto& selector = StorageDumpSelection(); + if (selector.address == 0 && selector.width == 0) return; + std::vector> ranges; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (const auto* texture : pending.textures) { + if (StorageDumpSelects(texture->descriptor)) ranges.emplace_back(texture->descriptor.baseAddress, static_cast(texture->guestBytes)); + } + } + for (const auto& [address, bytes] : ranges) static_cast(FlushPending(address, bytes, nullptr, reason)); +} + void StorageTexture::FlushAllPending(const char* reason) { static_cast(FlushPending(0, std::numeric_limits::max(), nullptr, reason)); } From 65b6a4729b60313253735055b7a79478353dadee Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 20:01:44 -0300 Subject: [PATCH 148/318] fix(libkernel): accept only the solo scheduling values the console accepts On a PS5 (firmware 13.60) scePthreadAttrSetsolosched and pthread_attr_setsolosched_np accept only 0 and 16; every other value returns ENOTSUP and keeps the stored one. The SCE set and get calls return EINVAL for a null or uninitialized attribute instead of throwing. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/src/Attr.cpp | 7 +++++-- core/libs/tests/GuestPthreadAttr.cpp | 17 +++++++++++++++-- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index ff5cba730..951b2016f 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -115,7 +115,7 @@ int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedPa } int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; if (!solosched) throw std::runtime_error(std::string(__func__) + ": null output"); *solosched = (*attr)->_solosched; return SCE_OK; @@ -161,7 +161,10 @@ int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy) { } int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int SoloSchedOff = 0; + constexpr int SoloSchedOn = 16; + if (solosched != SoloSchedOff && solosched != SoloSchedOn) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_solosched = solosched; return SCE_OK; } diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index ee24e0000..cb10c6b97 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -78,6 +78,8 @@ static void CheckStackSizeLimit() { Require(scePthreadAttrDestroy(&attr) == SCE_OK); } +extern "C" int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched); + static void CheckSoloSched() { PthreadAttr attr = nullptr; Require(pthread_attr_setsolosched_np_nid_postfix(nullptr, 1) == GUEST_EINVAL); @@ -85,10 +87,21 @@ static void CheckSoloSched() { Require(scePthreadAttrInit(&attr) == SCE_OK); int soloSched = -1; Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 0); - Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 1) == 0); - Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 1); + Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 16) == 0); + Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 16); + constexpr int GuestEopnotsupp = 45; + constexpr int SceKernelErrorEopnotsupp = static_cast(0x8002002Du); + for (int rejected : {-1, 1, 2, 4, 8, 15, 17, 32, 100, 0x7fffffff}) { + Require(pthread_attr_setsolosched_np_nid_postfix(&attr, rejected) == GuestEopnotsupp); + Require(scePthreadAttrSetsolosched(&attr, rejected) == SceKernelErrorEopnotsupp); + Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 16); + } Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 0) == 0); Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 0); + PthreadAttr unset = nullptr; + Require(scePthreadAttrSetsolosched(&unset, 16) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetsolosched(nullptr, 16) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetsolosched(&unset, &soloSched) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrDestroy(&attr) == SCE_OK); Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 1) == GUEST_EINVAL); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..c28200933 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -193,7 +193,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [sceKernelGetAvailableCpumask](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 -- [pthread_attr_setsolosched_np](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. Any value is accepted and threads created from the attribute are not scheduled differently; what solo scheduling does on the console and which values it rejects are unverified. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it +- [pthread_attr_setsolosched_np](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it - [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is the SDK 9 `libc.prx` heap (Astro Bot PPSA21564, PPSA22520), which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0 - [sceKernelMtypeprotect](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Map hints inside a reservation](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints From f7a85491ef7e1590e713e0dbd576e47d731f08d5 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:44:05 -0700 Subject: [PATCH 149/318] fix(libSceNet): report EAGAIN when a datagram receive times out on Windows With SO_RCVTIMEO set, Winsock fails a receive that waits that long with WSAETIMEDOUT, which native_error turned into ETIMEDOUT (60). sceNetRecv, sceNetRecvfrom and sceNetRecvmsg on a datagram socket therefore failed with 0x8041013C on Windows, where FreeBSD's sbwait returns EWOULDBLOCK (35) to soreceive, Linux returns EAGAIN, and POSIX (XSH 2.10.16) requires EAGAIN or EWOULDBLOCK when no data was received. A datagram socket has no connection that can time out, so WSAETIMEDOUT there only means that the receive timeout the guest set has expired: report EAGAIN. Stream sockets keep ETIMEDOUT, since Winsock leaves a connection in an indeterminate state after a timeout; TechnicalDebt records that gap and the one for SO_SNDTIMEO. --- core/libs/prx/libSceNet/Export.cpp | 15 ++++++++++++--- core/libs/tests/GuestSceNet.cpp | 19 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 3bfaa1229..23342a73c 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -364,6 +364,15 @@ std::int64_t message_length(const NetMsghdr* message) { return static_cast(total); } +int receive_error(const Sock& socket) { +#ifdef _WIN32 + if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.rcv_timeout_us > 0) return NET_EAGAIN; +#else + (void)socket; +#endif + return native_error(); +} + } // namespace extern "C" { @@ -649,7 +658,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); #endif - return result >= 0 ? result : fail(native_error()); + return result >= 0 ? result : fail(receive_error(socket)); } int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { @@ -672,7 +681,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); #endif - if (result < 0) return fail(native_error()); + if (result < 0) return fail(receive_error(socket)); if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; } @@ -760,7 +769,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { flags == NET_MSG_PEEK ? MSG_PEEK : 0, reinterpret_cast(&peer), &peer_length); if (received < 0) { #ifdef _WIN32 - if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(native_error()); + if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); received = static_cast(buffer.size()); #else return fail(native_error()); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 1579e6645..a75e95227 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -148,6 +148,24 @@ static void CheckEmptyEpoll() { } } +static void CheckReceiveTimeout(int receiver, int sender, const std::array& address) { + const int timeout = 50000; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + char buffer[8]{}; + std::array source{}; + std::uint32_t size = source.size(); + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{source.data(), static_cast(source.size()), in, 1, nullptr, 0, 0}; + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), 0), 35)); + Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), 0, source.data(), &size), 35)); + Require(Failed(sceNetRecvmsg(receiver, &receive, 0), 35)); + const char datagram[] = "late"; + Require(sceNetSendto(sender, datagram, sizeof(datagram), 0, address.data(), address.size()) == sizeof(datagram)); + Require(sceNetRecv(receiver, buffer, sizeof(buffer), 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + const int disabled = 0; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &disabled, sizeof(disabled)) == 0); +} + static void CheckMessages(int receiver, int sender, const std::array& address) { char head[] = "scatter "; char tail[] = "gather"; @@ -459,6 +477,7 @@ int main() { Require(stdio_readable[0] == 0 && stdio_writable[0] == 6); stdio_readable[0] = 1; Require(select_nid_postfix(1, stdio_readable, nullptr, nullptr, poll_now) == 0 && stdio_readable[0] == 0); + CheckReceiveTimeout(udp_receiver, udp_sender, address); CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); Require(sceNetSocketClose(udp_receiver) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..bcbe3f50a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -421,6 +421,7 @@ Throughout the project, every function at every stage either **does exactly what - [Storage-image units under a Linux import window](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) that the window stamped as a possible CPU store (`BlockMaybeWritten`) are compared with the bytes the image was uploaded from or written back as, and are unchanged when those still match. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Images tracked by layer instead of by 64 KiB unit still read the window as a change and upload again. - [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw +- [sceNetSetsockopt](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [Content export sessions](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified From 72d5b8ce9007a83b1721e445284f348f0f9e79d4 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:02:06 -0400 Subject: [PATCH 150/318] fix(relinker): ignore no-op relocations in guest modules --- core/relinker/CMakeLists.txt | 3 + .../relinker/src/guest/GuestImageReader.cpp | 12 +- .../tests/test_guest_none_relocations.py | 189 ++++++++++++++++++ 3 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_none_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 095915729..27889d99d 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -325,6 +325,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_rela_size.py $) add_test(NAME none_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_none_relocations.py $) + add_test(NAME guest_none_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_none_relocations.py $) + set_tests_properties(guest_none_relocations PROPERTIES TIMEOUT 20) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index e6c3545a7..52cf793e5 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -262,13 +262,15 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto size = get(sizeTag, sceSizeTag); if (size % 24 != 0) fail("Invalid relocation table size"); const auto offset = table(addressTag, sceAddressTag, size); - output.assign(bytes.begin() + offset, bytes.begin() + offset + size); + if (plt && size != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + output.reserve(size); for (std::uint64_t index = 0; index < size; index += 24) { - const auto info = Io::ReadU64(output, index + 8); + const auto info = Io::ReadU64(bytes, offset + index + 8); const auto relocation = static_cast(info); + if (relocation == 0) continue; if ((info >> 32) >= image.Symbols.size() || (plt && relocation != 7) || (relocation != 1 && relocation != 6 && relocation != 7 && relocation != 8 && relocation != 16 && relocation != 17 && relocation != 18)) fail("Unsupported relocation or symbol index"); - const auto target = Io::ReadU64(output, index); - const auto addend = Io::ReadU64(output, index + 16); + const auto target = Io::ReadU64(bytes, offset + index); + const auto addend = Io::ReadU64(bytes, offset + index + 16); mapped(target, 8, 2); const auto next = relocationTargets.lower_bound(target); if ((next != relocationTargets.end() && next->first < target + 8) || (next != relocationTargets.begin() && std::prev(next)->second > target)) fail("Overlapping guest relocations"); @@ -283,6 +285,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto& symbol = image.Symbols[info >> 32]; if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); } + output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); } }; if (tags.contains(7) || tags.contains(0x6100002f)) { @@ -290,7 +293,6 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } copyRelocations(7, 0x6100002f, 8, 0x61000031, image.Dynamic.RelaData, false); copyRelocations(23, 0x61000029, 2, 0x6100002d, image.Dynamic.RelaPltData, true); - if (!image.Dynamic.RelaPltData.empty() && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); if (tags.contains(12)) image.Init = tags.at(12); if (tags.contains(13)) image.Fini = tags.at(13); diff --git a/core/relinker/relinker/tests/test_guest_none_relocations.py b/core/relinker/relinker/tests/test_guest_none_relocations.py new file mode 100644 index 000000000..f11f79669 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_none_relocations.py @@ -0,0 +1,189 @@ +from pathlib import Path +import platform +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture, pe_sections +from test_guest_module_directories import module_with_symbol +from test_guest_symbol_names import dynamic_tags, file_offset +from test_macos_output import MachO + + +TARGET = 0x2500 +FUNCTION = 0x1000 +UNCHANGED = 0x8877665544332211 +NOOP = (0, 0, 0) +IGNORED = (0xffffffffffffffff, 0xffffffff00000000, -1) +OVERLAP = (TARGET + 1, 0xffffffff00000000, -1) +RELATIVE = (TARGET, 8, FUNCTION) +JUMP_SLOT = (TARGET, (1 << 32) | 7, 0) + + +def module(rela=(), plt=(), sce=False, lifecycle=False, initial=UNCHANGED): + image = module_with_symbol(True) + image.extend(bytes(0xE00 - len(image))) + struct.pack_into(" Date: Sat, 10 Oct 2026 01:07:23 -0700 Subject: [PATCH 151/318] fix(libc): map _Towctrans descriptors 1 and 2 to lower and upper case _Towctrans passed the guest's descriptor to the host towctrans. Dinkumware's (xwcc.h) defines towlower(wc) as _Towctrans(wc, 1) and towupper(wc) as _Towctrans(wc, 2), the values FreeBSD's wctrans.c also uses for "tolower" and "toupper". UCRT reads 1 as toupper and other nonzero values as tolower, so on Windows towlower(L'A') returned L'A' and towupper(L'a') returned L'a', while towlower(L'a') returned L'A'. The host wint_t is 16 bits there, which also turned WEOF into 0xffff. glibc's wctrans_t is a pointer to a table that towctrans reads, so on Linux descriptor 1 or 2 is dereferenced as an address. _Towctrans now takes the guest's 32-bit wint_t and size_t descriptor. 1 and 2 map 0..255 through the C locale tables behind _Getptolower and _Getptoupper and return other values unchanged. Any other descriptor sets errno to EINVAL and returns wc unchanged, as FreeBSD's towctrans_l does. --- core/libs/prx/libc/src/LocaleSupport.cpp | 13 +++++++++--- core/libs/tests/GuestLocale.cpp | 26 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index d0a70841a..a21ce8227 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include @@ -323,8 +323,15 @@ mbstate_t* APS5_VABI _Getpwcstate_nid_postfix() { return &state; } -wint_t APS5_VABI _Towctrans_nid_postfix(wint_t c, wctrans_t desc) { - return std::towctrans(c, desc); +std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc) { + constexpr std::uint64_t ToLower = 1; + constexpr std::uint64_t ToUpper = 2; + if (desc != ToLower && desc != ToUpper) { + errno = 22; + return c; + } + if (c > UCHAR_MAX) return c; + return static_cast((desc == ToLower ? g_lowerTable : g_upperTable)[c + 1]); } // Locale ids and stream objects the title imports besides the ones above; the streams are zeroed diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 632670181..93cf43ff1 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/GuestLocale.hpp" #include +#include #include #include #include @@ -23,6 +24,8 @@ int APS5_VABI _Mbtowcx_nid_postfix(std::uint16_t* dst, const char* src, std::siz int APS5_VABI _Wctombx_nid_postfix(char* dst, std::uint16_t src, std::mbstate_t* st); void APS5_VABI _Locksyslock_nid_postfix(); void APS5_VABI _Unlocksyslock_nid_postfix(); +std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t wc, std::uint64_t desc); +int* APS5_VABI __error_nid_postfix(); } @@ -144,12 +147,35 @@ static void CheckStreamDestruction() { Require(object.stream.locale == &_ZSt21_sceLibcClassicLocale_nid_postfix); } +static void CheckWideCaseTranslation() { + const auto* lower = _Getptolower_nid_postfix(); + const auto* upper = _Getptoupper_nid_postfix(); + Require(_Towctrans_nid_postfix(u'A', 1) == u'a' && _Towctrans_nid_postfix(u'a', 2) == u'A'); + *__error_nid_postfix() = 0; + for (std::uint32_t value = 0; value < 256; ++value) { + Require(_Towctrans_nid_postfix(value, 1) == static_cast(lower[value])); + Require(_Towctrans_nid_postfix(value, 2) == static_cast(upper[value])); + } + const std::uint32_t uncased[] = {0x100u, 0x391u, 0x3b1u, 0xffffu, 0xffffffffu}; + for (const std::uint32_t value : uncased) { + Require(_Towctrans_nid_postfix(value, 1) == value && _Towctrans_nid_postfix(value, 2) == value); + } + Require(*__error_nid_postfix() == 0); + const std::uint64_t invalidDescriptors[] = {0, 3}; + for (const std::uint64_t descriptor : invalidDescriptors) { + *__error_nid_postfix() = 0; + Require(_Towctrans_nid_postfix(u'A', descriptor) == u'A' && _Towctrans_nid_postfix(u'7', descriptor) == u'7'); + Require(*__error_nid_postfix() == 22); + } +} + int main() { CheckGuestCalls(); CheckLocinfoAlignment(); CheckCharacterTables(); CheckCharacterConversions(); CheckStreamDestruction(); + CheckWideCaseTranslation(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); From 7cc5698094d6859b757d4d5a404ebaf0a852341e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:30:01 -0700 Subject: [PATCH 152/318] fix(libc): format years before 1900 in asctime on Windows asctime passed the guest tm to the host CRT. UCRT's asctime returns null and sets errno to EINVAL when tm_year is negative or tm_mday is past the end of the month (common_asctime_s in ucrt/time/asctime.cpp), so on Windows asctime of any time in the years 1000 to 1899, which gmtime converts, or of February 29 in a common year returned null. C17 7.27.3.1 defines the text for every year from 1000 to 9999 when each member is in its normal range (tm_mday 1 to 31), and glibc on the Linux build and FreeBSD 11's asctime (contrib/tzcode/stdtime/asctime.c) print it. Format the text with the C17 algorithm's format into a thread-local buffer that fits any member values. As in glibc and FreeBSD, a weekday or month outside its range is printed as ???, and the year is computed in 64 bits. --- core/libs/prx/libc/src/Time.cpp | 11 +++++++++-- core/libs/tests/GuestTimeConversion.cpp | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index b730a9066..be7c8cea4 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -17,6 +17,9 @@ namespace { +constexpr const char* WEEKDAY_NAMES[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +constexpr const char* MONTH_NAMES[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; +constexpr std::size_t ASCTIME_SIZE = sizeof("Sun Jan-2147483648 -2147483648:-2147483648:-2147483648 -2147483648\n"); constexpr std::int64_t SECONDS_PER_DAY = 86400; constexpr std::int64_t DAYS_FROM_MARCH_TO_YEAR_END = 306; @@ -308,8 +311,12 @@ size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* } char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr) { - const std::tm host = toHostTm(*timeptr); - return std::asctime(&host); + static thread_local char result[ASCTIME_SIZE]; + const char* weekday = timeptr->tm_wday >= 0 && timeptr->tm_wday < 7 ? WEEKDAY_NAMES[timeptr->tm_wday] : "???"; + const char* month = timeptr->tm_mon >= 0 && timeptr->tm_mon < 12 ? MONTH_NAMES[timeptr->tm_mon] : "???"; + std::snprintf(result, sizeof(result), "%s %s%3d %.2d:%.2d:%.2d %lld\n", weekday, month, timeptr->tm_mday, + timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec, 1900LL + timeptr->tm_year); + return result; } char* APS5_VABI ctime_nid_postfix(const int64_t* timer) { diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index e0027fb55..1989e7f63 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -19,6 +19,7 @@ GuestTm* APS5_VABI libc_localtime_nid_postfix(const std::int64_t*); GuestTm* APS5_VABI localtime_nid_postfix(const std::int64_t*); GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t*, GuestTm*); GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); +char* APS5_VABI asctime_nid_postfix(const GuestTm*); std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); void APS5_VABI tzset_nid_postfix(void); @@ -68,6 +69,17 @@ static void CheckConcurrent(Converter convert, BufferedConverter buffered, const Require(convert(&invalid) == nullptr, "Unrepresentable time was accepted"); } +static GuestTm UtcTime(std::int64_t timer) { + GuestTm utc{}; + Require(gmtime_s_nid_postfix(&timer, &utc) == &utc, "UTC conversion failed"); + return utc; +} + +static void CheckAsctime(const GuestTm& brokenDown, const char* expected) { + const char* text = asctime_nid_postfix(&brokenDown); + Require(text != nullptr && std::strcmp(text, expected) == 0, "asctime did not format the broken-down time"); +} + struct UtcCase { std::int64_t timer; int year; @@ -202,6 +214,14 @@ int main() { GuestTm scratch{}; Require(gmtime_s_nid_postfix(&underflow, &scratch) == nullptr, "Unrepresentable negative time was accepted"); + CheckAsctime(UtcTime(0), "Thu Jan 1 00:00:00 1970\n"); + CheckAsctime(UtcTime(-2208988801), "Sun Dec 31 23:59:59 1899\n"); + CheckAsctime(UtcTime(-30610224000), "Wed Jan 1 00:00:00 1000\n"); + GuestTm pastMonthEnd = UtcTime(983404800); + pastMonthEnd.tm_mon = 1; + pastMonthEnd.tm_mday = 29; + CheckAsctime(pastMonthEnd, "Thu Feb 29 00:00:00 2001\n"); + const std::int64_t epoch = 0; GuestTm local{}; std::memset(&local, 0xAA, sizeof(local)); From 1b009e1879a2c76da74dabb4247dc39cb7fa4a4d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:21:59 -0700 Subject: [PATCH 153/318] fix(libc): subtract negative times in difftime on Windows difftime and libc_difftime passed their arguments to the host CRT. UCRT's _difftime64 returns 0 and sets errno to EINVAL when either argument is negative (common_difftime in ucrt/time/difftime.cpp), so on Windows difftime(0, -1) returned 0 instead of 1 and every difference involving a time before 1970 was 0. C17 7.27.2.2 defines the result as time1 - time0, and FreeBSD 11's difftime (contrib/tzcode/stdtime/difftime.c) and glibc on the Linux build compute it for any arguments. Subtract the smaller time from the larger as uint64_t, which cannot overflow, convert the difference to double and negate it when time1 is the smaller one, as FreeBSD and glibc's fallback path do. The result is time1 - time0 rounded once to double on both hosts. difftime now forwards to libc_difftime like the other aliases in the file. --- core/libs/prx/libc/src/Time.cpp | 5 +++-- core/libs/tests/GuestTimeConversion.cpp | 28 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index b730a9066..fe142d54c 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -231,11 +231,12 @@ int64_t APS5_VABI _Xtime_get_ticks_nid_postfix() { } double APS5_VABI libc_difftime_nid_postfix(int64_t time1, int64_t time0) { - return std::difftime(static_cast(time1), static_cast(time0)); + if (time1 >= time0) return static_cast(static_cast(time1) - static_cast(time0)); + return -static_cast(static_cast(time0) - static_cast(time1)); } double APS5_VABI difftime_nid_postfix(int64_t time1, int64_t time0) { - return std::difftime(static_cast(time1), static_cast(time0)); + return libc_difftime_nid_postfix(time1, time0); } GuestTm* APS5_VABI libc_gmtime_nid_postfix(const int64_t* timer) { diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index e0027fb55..6f38a8d3f 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -24,6 +24,8 @@ std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, cons void APS5_VABI tzset_nid_postfix(void); char* APS5_VABI ctime_nid_postfix(const std::int64_t*); int* APS5_VABI __error_nid_postfix(); +double APS5_VABI libc_difftime_nid_postfix(std::int64_t, std::int64_t); +double APS5_VABI difftime_nid_postfix(std::int64_t, std::int64_t); } using Converter = GuestTm* (APS5_VABI *)(const std::int64_t*); @@ -92,6 +94,17 @@ static void CheckUtc(const UtcCase& expected) { Require(utc.tm_zone != nullptr && std::strcmp(utc.tm_zone, "UTC") == 0, "UTC conversion did not set tm_zone"); } +struct DifferenceCase { + std::int64_t time1; + std::int64_t time0; + double seconds; +}; + +static void CheckDifference(const DifferenceCase& expected) { + Require(libc_difftime_nid_postfix(expected.time1, expected.time0) == expected.seconds + && difftime_nid_postfix(expected.time1, expected.time0) == expected.seconds, "difftime did not return time1 - time0"); +} + static void CheckFormat(const GuestTm& guest, const char* format, const char* expected) { char buffer[96]{}; const std::size_t size = strftime_nid_postfix(buffer, sizeof(buffer), format, &guest); @@ -226,6 +239,21 @@ int main() { Require(std::strcmp(ctime_nid_postfix(&underflow), "??? ??? ?? ??:??:?? ????\n") == 0 && *__error_nid_postfix() == 22, "ctime of an unrepresentable time did not report it"); + constexpr std::int64_t latest = std::numeric_limits::max(); + constexpr std::int64_t earliest = std::numeric_limits::min(); + constexpr std::array differenceCases{{ + {10, 3, 7.0}, + {3, 10, -7.0}, + {0, -1, 1.0}, + {-1, 0, -1.0}, + {-86400, -172800, 86400.0}, + {latest, latest - 1, 1.0}, + {earliest + 1, earliest, 1.0}, + {latest, earliest, 0x1p64}, + {earliest, latest, -0x1p64}, + }}; + for (const auto& differenceCase : differenceCases) CheckDifference(differenceCase); + GuestTm utc{}; Require(gmtime_s_nid_postfix(&epoch, &utc) == &utc, "UTC epoch conversion failed"); char formatted[64]{}; From 8b04156bfd2e65eb851efc56bd26531551a23279 Mon Sep 17 00:00:00 2001 From: virp-mta Date: Mon, 12 Oct 2026 03:12:34 +0400 Subject: [PATCH 154/318] fix(libkernel): return EDESTADDRREQ for send and sendto on unconnected UDP sockets --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 12 ++++++++++-- core/libs/tests/GuestUdp.cpp | 5 +++-- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 52f37a428..e289a370d 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -60,6 +60,14 @@ int NativeError() { return GuestSockets::GuestErrorFromHost(errno); #endif } +int SendError(int type) { +#ifdef _WIN32 + if (type == 2 && WSAGetLastError() == WSAENOTCONN) return 39; +#else + static_cast(type); +#endif + return NativeError(); +} struct Socket { NativeSocket value; int family; @@ -597,7 +605,7 @@ std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std: if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), NativeSendFlags(flags)); - return result < 0 ? Fail(NativeError()) : result; + return result < 0 ? Fail(SendError(socket->type)) : result; } std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); @@ -690,7 +698,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), NativeSendFlags(flags), reinterpret_cast(&native), size)); } - return result < 0 ? Fail(NativeError()) : result; + return result < 0 ? Fail(SendError(socket->type)) : result; } std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags, void* address, std::uint32_t* addressLength) { diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 2f69a2201..7b86637aa 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -15,6 +15,7 @@ int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); int APS5_VABI _ioctl_nid_postfix(int, std::uint64_t, void*); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); int APS5_VABI close_nid_postfix(int); @@ -68,13 +69,13 @@ int main() { Require(getsockopt_nid_postfix(sender, 0xffff, 0x20, &option, &optionSize) == 0 && option != 0); Require(setsockopt_nid_postfix(sender, 0xffff, 12345, &enabled, sizeof(enabled)) == -1); Require(*__error_nid_postfix() == 42); -#ifndef _WIN32 const int unconnected = socket_nid_postfix(2, 2, 0); Require(unconnected >= 0); + Require(send_nid_postfix(unconnected, "x", 1, 0) == -1); + Require(*__error_nid_postfix() == 39); Require(sendto_nid_postfix(unconnected, "x", 1, 0, nullptr, 0) == -1); Require(*__error_nid_postfix() == 39); Require(close_nid_postfix(unconnected) == 0); -#endif const int unsized = socket_nid_postfix(2, 2, 0); std::array unsizedAddress{0, 2, 0, 0, 127, 0, 0, 1}; Require(unsized >= 0 && bind_nid_postfix(unsized, unsizedAddress.data(), unsizedAddress.size()) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..5615d50ea 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -607,6 +607,7 @@ Throughout the project, every function at every stage either **does exactly what - [The scanf family](../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [socketpair](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. +- [send, sendto](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). - [read, write and vector I/O](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [Socket error numbers](../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 From 802ec551c2e9837249bbcb57f747a1a0f2965e8b Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 20:12:43 -0300 Subject: [PATCH 155/318] fix(libSceUserService): answer sceUserServiceGetUserName as the console does On a PS5 (firmware 13.60) a null name returns INVALID_ARGUMENT first, then a user id the console does not know returns 0x80960105 whatever the buffer, and a buffer shorter than 17 bytes is zeroed before BUFFER_TOO_SHORT is returned. Here an unknown user returned INVALID_ARGUMENT and a short buffer was left untouched. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceUserService/CMakeLists.txt | 11 +++++ core/libs/prx/libSceUserService/Export.cpp | 6 ++- .../prx/libSceUserService/UserService.hpp | 1 + core/libs/tests/GuestUserServiceUserName.cpp | 49 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestUserServiceUserName.cpp diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index 444c378c2..ac0ab70e9 100644 --- a/core/libs/prx/libSceUserService/CMakeLists.txt +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -12,3 +12,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_user_service_user_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceUserName.cpp) + target_include_directories(guest_user_service_user_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_user_service_user_name_tests PRIVATE libSceUserService) + add_test(NAME guest_user_service_user_name COMMAND guest_user_service_user_name_tests) + if(WIN32) + set_tests_properties(guest_user_service_user_name PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 2a54f7c9f..e3b110d81 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -117,10 +117,14 @@ int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_ } int APS5_VABI sceUserServiceGetUserName(int user_id, char* name, size_t size) { - if (name == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + if (name == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } + if (user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_UNKNOWN_USER; + } if (size < USER_SERVICE_MAX_USER_NAME_LENGTH + 1) { + std::memset(name, 0, size); return USER_SERVICE_ERROR_BUFFER_TOO_SHORT; } std::strncpy(name, USER_SERVICE_INITIAL_USER_NAME, size); diff --git a/core/libs/prx/libSceUserService/UserService.hpp b/core/libs/prx/libSceUserService/UserService.hpp index dcc140dad..8c4c9c0e2 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -10,6 +10,7 @@ constexpr int USER_SERVICE_ERROR_NO_EVENT = -2137653241; constexpr int USER_SERVICE_USER_ID_INVALID = -1; constexpr int USER_SERVICE_INITIAL_USER_ID = 0x10000000; constexpr int USER_SERVICE_ERROR_BUFFER_TOO_SHORT = -2137653238; +constexpr int USER_SERVICE_ERROR_UNKNOWN_USER = static_cast(0x80960105u); constexpr std::size_t USER_SERVICE_MAX_USER_NAME_LENGTH = 16; constexpr const char* USER_SERVICE_INITIAL_USER_NAME = "Player"; diff --git a/core/libs/tests/GuestUserServiceUserName.cpp b/core/libs/tests/GuestUserServiceUserName.cpp new file mode 100644 index 000000000..f75516297 --- /dev/null +++ b/core/libs/tests/GuestUserServiceUserName.cpp @@ -0,0 +1,49 @@ +#include "SceTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUserServiceGetUserName(int user_id, char* name, std::size_t size); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +bool Filled(const char* buffer, std::size_t from, std::size_t to, char value) { + for (std::size_t i = from; i < to; ++i) { + if (buffer[i] != value) return false; + } + return true; +} + +} + +int main() { + char name[32]; + const std::size_t length = std::strlen(USER_SERVICE_INITIAL_USER_NAME); + + for (std::size_t size = 0; size <= USER_SERVICE_MAX_USER_NAME_LENGTH; ++size) { + std::memset(name, 'A', sizeof(name)); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, size) == USER_SERVICE_ERROR_BUFFER_TOO_SHORT); + Require(Filled(name, 0, size, 0) && Filled(name, size, sizeof(name), 'A')); + } + for (std::size_t size : {USER_SERVICE_MAX_USER_NAME_LENGTH + 1, sizeof(name)}) { + std::memset(name, 'A', sizeof(name)); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, size) == USER_SERVICE_OK); + Require(std::strcmp(name, USER_SERVICE_INITIAL_USER_NAME) == 0); + Require(Filled(name, length, size, 0) && Filled(name, size, sizeof(name), 'A')); + } + + for (int user : {0, USER_SERVICE_USER_ID_INVALID, 1, 0xff, 0x12345678}) { + std::memset(name, 'A', sizeof(name)); + Require(sceUserServiceGetUserName(user, name, sizeof(name)) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetUserName(user, name, 1) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(Filled(name, 0, sizeof(name), 'A')); + Require(sceUserServiceGetUserName(user, nullptr, sizeof(name)) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + } + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, nullptr, sizeof(name)) == USER_SERVICE_ERROR_INVALID_ARGUMENT); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..cc1522eee 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -291,6 +291,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceHttpSetRedirectCallback, sceHttpSetCookieRecvCallback, sceHttpsSetSslVersion, sceHttpsGetSslError](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), which only logs and returns 0; a null `errNum` or `detail` in `sceHttpsGetSslError` is assumed to return `SCE_HTTP_ERROR_INVALID_VALUE` like `sceHttpGetLastErrno` - [sceHttpSetAuthInfoCallback](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature and callback type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4); no PS5 reference has them. The `id` is not checked, as in the other libSceHttp setters - [libSceHttp2 cookie boxes](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` is taken to take the library context id first, as in the [prosper](https://github.com/mattias800/prosper) reimplementation; further arguments are unknown and ignored. `sceHttp2SetRequestNoContentLength` is assumed to take only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard +- [sceUserServiceGetUserName](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceNpSessionSignalingGetMemoryInfo](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the meaning of the value is unknown; 0 is reported, as for the unset game presets. PPSA12544 (Unity) only stores it for its user profile From c0c86deeb82714ae54f1faf802c2dfe3fcb3230e Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:03:44 -0400 Subject: [PATCH 156/318] test(recompiler): cover implicit inputs in relative vector windows --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../libSceAgcDriver/tests/VectorWindow.cpp | 147 +++++++++++++++++- .../tests/execution/RelativeStageInputs.cpp | 147 ++++++++++++++++++ 3 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 20c42fc92..73a58caaf 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1083,6 +1083,12 @@ if(BUILD_TESTING) endif() endforeach() endforeach() + add_test(NAME agc_vector_window_stage_inputs COMMAND agc_vector_window_tests --stage-inputs-only) + set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_vector_window_stage_inputs agc_driver_relative_stage_inputs PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index 3e852184b..8d29aa216 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -1,9 +1,13 @@ #include "Recompiler.hpp" +#include "ControlFlow/GraphBuilder.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Translation/InstructionTranslator.hpp" #include #include #include #include #include +#include #include #include #include @@ -91,6 +95,144 @@ Shape Scan(std::span words) { return shape; } +IrProgram TranslateStage(ShaderStageKind stage, std::uint32_t waveSize, std::uint32_t base = 0u, std::uint32_t fetchStart = 0u, std::uint32_t fetchCount = 0u) { + ShaderComputeInputInfo compute{}; + compute.threadIdsNum = 3; + compute.threadsNum[0] = waveSize; + ShaderVertexInputInfo vertex{}; + vertex.mesh.threadsNum[0] = waveSize; + vertex.mesh.inputPrimitive = 1u; + vertex.mesh.primitivesPerGroup = 1u; + vertex.mesh.verticesPerGroup = 1u; + vertex.mesh.esgsItemSize = 1u; + ShaderPixelInputInfo pixel{}; + std::uint32_t next = 0u; + for (const auto input : {PixelInput::PerspectiveCenter, PixelInput::PerspectiveCentroid, PixelInput::LinearCenter, PixelInput::LinearCentroid, + PixelInput::PositionX, PixelInput::PositionY, PixelInput::PositionZ, PixelInput::PositionW, + PixelInput::FrontFace, PixelInput::Ancillary, PixelInput::LineStipple, PixelInput::PositionFixedPoint}) { + pixel.psInputVgpr[static_cast(input)] = next; + next += PixelInputVgprCount(input); + } + std::vector code{MoveM0Two}; + if (fetchCount != 0u) { + code.push_back(0xbe802102u); + code.push_back(0x7e008700u | base); + code.push_back(0x7e1002f2u); + vertex.resourcesNum = 1; + vertex.resourcesDst[0] = {static_cast(fetchStart), static_cast(fetchCount), 0, 0u}; + } + code.push_back(0x7e008700u | base); + code.push_back(EndProgram); + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + if (fetchCount != 0u) { + const auto& call = decoded.instructions.at(1); + Require(call.op == RdnaOpcode::SSwappcB64 && call.family == RdnaInstructionFamily::SOP1 && call.opcodeId == 0x21u && + call.destination.kind == RdnaOperandKind::ScalarRegister && call.destination.reg == 0u && + call.source0.kind == RdnaOperandKind::ScalarRegister && call.source0.reg == 2u, + "external fetch fixture did not decode s_swappc_b64 s[0:1], s[2:3]"); + } + const SwappcInfo swappc{fetchCount != 0u, 0u, 8u}; + const auto graph = GraphBuilder{}.Build(decoded, &swappc); + Require(graph.hasFetchCall == (fetchCount != 0u), "stage fixture did not identify its external fetch call"); + TranslateOptions options{}; + options.stage = stage; + options.waveSize = waveSize; + options.userDataCount = 8u; + options.inputInfo = {&vertex, &pixel, &compute}; + return InstructionTranslator{}.Translate(decoded, graph, options); +} + +void RequireWindow(const IrProgram& program, std::uint32_t base, std::uint32_t limit, std::span initialized) { + Require(program.Metadata().vectorWindowBase == base && program.Metadata().vectorWindowSize == limit - base, "relative window does not include exactly the initialized stage registers"); + std::array stored{}; + for (const auto* instruction : program.BlockOrder().front()->Instructions()) { + if (instruction->Opcode() == IrOpcode::StoreVectorWindow) { + Require(instruction->Argument(0)->HasImmediate(), "stage input uses a dynamic window index"); + const auto index = base + instruction->Argument(0)->ImmediateU32(); + Require(index < limit, "stage input lies past the window"); + stored[index] = true; + } else if (instruction->Opcode() == IrOpcode::SetVectorRegister) { + const auto index = instruction->Argument(0)->Register().index; + Require(index < base, "stage input escaped the relative window into SSA"); + stored[index] = true; + } + } + for (const auto index : initialized) Require(stored[index], "stage input v" + std::to_string(index) + " was not initialized"); + bool bounded = false; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() == IrOpcode::ULessThan32 && instruction->Argument(1)->HasImmediate() && instruction->Argument(1)->ImmediateU32() == limit - base) bounded = true; + } + } + Require(bounded, "relative access did not use the final stage window bound"); +} + +template +bool RunStage(const char* name, TAction action) { + try { + action(); + std::printf("vector window stage inputs: %s passed\n", name); + return true; + } catch (const std::exception& error) { + std::fprintf(stderr, "vector window stage inputs: %s: %s\n", name, error.what()); + return false; + } +} + +bool StageInputs() { + bool passed = true; + const auto stage = [&](const char* name, ShaderStageKind kind, std::uint32_t limit, std::initializer_list initialized) { + passed &= RunStage(name, [&] { + for (const auto wave : {32u, 64u}) RequireWindow(TranslateStage(kind, wave), 0u, limit, {initialized.begin(), initialized.size()}); + }); + }; + stage("compute", ShaderStageKind::Compute, 3u, {0u, 1u, 2u}); + stage("vertex", ShaderStageKind::Vertex, 9u, {5u, 8u}); + stage("local", ShaderStageKind::Local, 6u, {2u, 3u, 5u}); + stage("tessellation control", ShaderStageKind::TessellationControl, 2u, {0u, 1u}); + stage("tessellation evaluation", ShaderStageKind::TessellationEvaluation, 9u, {0u, 5u, 6u, 7u, 8u}); + stage("mesh", ShaderStageKind::Mesh, 9u, {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}); + stage("pixel", ShaderStageKind::Pixel, 16u, {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u, 12u, 13u, 14u, 15u}); + passed &= RunStage("SSA prefix", [] { + const std::array local{2u, 3u, 5u}; + const std::array vertex{5u, 8u}; + for (const auto wave : {32u, 64u}) { + RequireWindow(TranslateStage(ShaderStageKind::Local, wave, 4u), 4u, 6u, local); + RequireWindow(TranslateStage(ShaderStageKind::Vertex, wave, 16u), 16u, 17u, vertex); + } + }); + passed &= RunStage("external fetch", [] { + const std::array vertex{5u, 8u}; + for (const auto wave : {32u, 64u}) { + for (const auto start : {20u, 252u}) { + for (const auto count : {1u, 2u, 3u, 4u}) { + const auto program = TranslateStage(ShaderStageKind::Vertex, wave, 0u, start, count); + RequireWindow(program, 0u, start + count, vertex); + std::uint32_t fetched = 0u; + std::uint32_t boundedReads = 0u; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() == IrOpcode::StoreVectorWindow) { + const auto* value = instruction->Argument(1); + if (value->Opcode() == IrOpcode::SelectU32) value = value->Argument(1); + if (value->Opcode() == IrOpcode::GetAttribute) { + Require(instruction->Argument(0)->ImmediateU32() == start + fetched && value->Argument(1)->ImmediateU32() == fetched, + "external fetch did not initialize its entire window span"); + ++fetched; + } + } + if (instruction->Opcode() == IrOpcode::ULessThan32 && instruction->Argument(1)->HasImmediate() && instruction->Argument(1)->ImmediateU32() == start + count) ++boundedReads; + } + } + Require(fetched == count && boundedReads == 2u, "relative accesses to the external fetch disagree about the window"); + } + } + } + }); + if (passed) std::puts("Vector window stage input and fetch tests passed"); + return passed; +} + bool run(const char* name, bool (*test)()) { try { if (test()) return true; @@ -103,7 +245,9 @@ bool run(const char* name, bool (*test)()) { } -int main() { +int main(int argc, char** argv) { + if (argc == 2 && std::string_view(argv[1]) == "--stage-inputs-only") return StageInputs() ? 0 : 1; + if (argc != 1) return 2; bool passed = true; passed &= run("a relative read and write index a function-scope window instead of selecting over the registers", [] { @@ -119,5 +263,6 @@ int main() { return shape.functionArrays == 0 && shape.accessChains == 0; }); + if (passed) std::puts("Vector window shape and SSA control tests passed"); return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp new file mode 100644 index 000000000..0d919f5d1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp @@ -0,0 +1,147 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Extent = 16u; +alignas(256) std::array pixels{}; +alignas(256) std::array output{}; +alignas(256) constexpr std::array, 6> positions{{ + {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +constexpr std::array relativeVertexCode{ + 0xbefc0385u, 0x7e028700u, 0xbefc0388u, 0x7e008700u, 0x4a000300u, + 0xe0382000u, 0x80000000u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u +}; +constexpr std::array vertexCode{ + 0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u +}; +constexpr std::array relativePixelCode{ + 0xbefc0383u, 0x7e008700u, 0xf800180fu, 0x00000000u, 0xbf810000u +}; +constexpr std::array pixelCode{ + 0x7e0002f2u, 0xf800180fu, 0x00000000u, 0xbf810000u +}; +constexpr std::array computeCode{ + 0xbefc0382u, 0x7e028700u, 0x34020283u, 0x4a000300u, 0x34000082u, + 0x7e028700u, 0xe0701000u, 0x80000100u, 0xbf810000u +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, bool relativeVertex) { + pixels.fill(std::byte{0x40}); + const auto target = device.Target(); + const auto userData = BufferDescriptor(positions.data(), 16u, positions.size()); + const auto code = relativeVertex ? std::span(relativeVertexCode) : std::span(vertexCode); + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(code.data()), code, 0, {}}, + {32u, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, {}}, + target, {0u, 0u, 0u, 64u} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto pushBytes = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + if (!relativeVertex) { + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) + | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY) + | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionZ) + | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionW); + pixel.posX = pixel.posY = pixel.posZ = pixel.posW = true; + } + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + const auto fragmentCode = relativeVertex ? std::span(pixelCode) : std::span(relativePixelCode); + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(fragmentCode.data()), fragmentCode, 0, {}}, + {32u, 0u, {}, std::nullopt, pixel, std::nullopt, {}}, target, PixelPushLayout(pushBytes, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0u}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(pushBytes, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Extent, Extent}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false}; + draw.firstVertex = relativeVertex ? 1u : 3u; + draw.firstInstance = 2u; + device.Draw(state, draw, shaders); + device.WaitIdle(); + for (const auto value : pixels) Require(value == std::byte{255}, "relative stage input draw did not cover every pixel in white"); +} + +void Dispatch(AgcDriver::VulkanDevice& device) { + output.fill(0xdeadbeefu); + const auto userData = BufferDescriptor(output.data(), 0u, sizeof(output)); + const ShaderRecompiler::ShaderComputeStageInfo compute{{8u, 1u, 2u}, 0u, {false, false, false}, false, 3u}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(computeCode.data()), computeCode, 0, {}}, + {32u, 0u, userData, compute, std::nullopt, std::nullopt, {}}, device.Target(), {0u, 0u, 0u, 128u} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1u, 1u, 1u); + device.WaitIdle(); + for (std::size_t index = 0; index < output.size(); ++index) { + Require(output[index] == index / 8u, "relative stage input dispatch did not preserve the implicit Z input at output " + std::to_string(index)); + } +} + +template +bool Run(const char* name, TAction action) { + try { + action(); + std::cout << "relative stage inputs: " << name << " GPU readback passed\n"; + return true; + } catch (const std::exception& error) { + std::cerr << "relative stage inputs: " << name << ": " << error.what() << '\n'; + return false; + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + bool passed = Run("vertex", [&] { Draw(*device, true); }); + passed &= Run("pixel", [&] { Draw(*device, false); }); + passed &= Run("compute", [&] { Dispatch(*device); }); + if (!passed) return 1; + std::cout << "relative stage input tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From e4575c723e146774fed1c8163be3f43826c018d7 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:14:08 -0400 Subject: [PATCH 157/318] test(recompiler): keep vector window fixtures observable --- .../libSceAgcDriver/tests/VectorWindow.cpp | 26 ++++++++++++------- .../tests/execution/RelativeStageInputs.cpp | 7 ++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index 8d29aa216..7f622c956 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -19,11 +19,12 @@ namespace { using namespace ShaderRecompiler; constexpr std::uint32_t MoveM0Two = 0xbefc0382u; +constexpr std::uint32_t MoveM0GroupX = 0xbefc0304u; constexpr std::uint32_t MoveV4One = 0x7e0802f2u; constexpr std::uint32_t MoveV5Two = 0x7e0a02f4u; constexpr std::uint32_t MoveV6Four = 0x7e0c02f6u; constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u; -constexpr std::uint32_t MovreldV4V1 = 0x7e088501u; +constexpr std::uint32_t MovreldV4V0 = 0x7e088500u; constexpr std::uint32_t MoveV1V5 = 0x7e020305u; constexpr std::uint32_t EndProgram = 0xbf810000u; @@ -40,14 +41,14 @@ void Require(bool condition, const std::string& message) { std::vector RecompileProgram(std::span code) { const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformVote, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses}; - const std::array userData{}; + const std::array userData{0x10000000u, 0u, 0x40u, 0x31016facu}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; request.context.waveSize = 32; request.context.userDataBaseRegister = 0; request.context.userData = userData; - request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 4u, {false, false, false}, false, 1u}; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 4u, {true, false, false}, false, 1u}; request.target.vulkanVersion = 0x00403000u; request.target.spirvVersion = 0x00010600u; request.target.subgroupSize = 32; @@ -69,6 +70,7 @@ Shape Scan(std::span words) { std::vector> constants; std::vector> arrays; std::vector> functionPointers; + std::vector windowVariables; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> spv::WordCountShift; const auto op = static_cast(words[cursor] & spv::OpCodeMask); @@ -82,13 +84,18 @@ Shape Scan(std::span words) { for (const auto& [array, length] : arrays) { if (array != pointee) continue; ++shape.functionArrays; + windowVariables.push_back(words[cursor + 2]); for (const auto& [constant, value] : constants) { if (constant == length) shape.arrayLength = value; } } } } - if (op == spv::OpAccessChain) ++shape.accessChains; + if (op == spv::OpAccessChain) { + for (const auto variable : windowVariables) { + if (words[cursor + 3] == variable) ++shape.accessChains; + } + } if (op == spv::OpSelect) ++shape.selects; cursor += count; } @@ -250,15 +257,16 @@ int main(int argc, char** argv) { if (argc != 1) return 2; bool passed = true; - passed &= run("a relative read and write index a function-scope window instead of selecting over the registers", [] { - const std::array code{MoveM0Two, MoveV4One, MoveV5Two, MoveV6Four, MovrelsV1V4, MovreldV4V1, MoveV1V5, EndProgram}; + passed &= run("observable relative reads and writes index a function-scope window", [] { + const std::array code{MoveM0GroupX, MoveV4One, MoveV5Two, MoveV6Four, MovrelsV1V4, + 0xe0700000u, 0x80000100u, MovreldV4V0, MoveV1V5, 0xe0700004u, 0x80000100u, EndProgram}; const auto shape = Scan(RecompileProgram(code)); - if (shape.functionArrays != 1 || shape.arrayLength != 3 || shape.accessChains < 2 || shape.selects > 4) std::fprintf(stderr, " window shape: arrays %zu length %u access chains %zu selects %zu\n", shape.functionArrays, shape.arrayLength, shape.accessChains, shape.selects); - return shape.functionArrays == 1 && shape.arrayLength == 3 && shape.accessChains >= 2 && shape.selects <= 4; + if (shape.functionArrays != 1 || shape.arrayLength != 3 || shape.accessChains < 2) std::fprintf(stderr, " window shape: arrays %zu length %u access chains %zu selects %zu\n", shape.functionArrays, shape.arrayLength, shape.accessChains, shape.selects); + return shape.functionArrays == 1 && shape.arrayLength == 3 && shape.accessChains >= 2; }); passed &= run("a program without relative addressing keeps its registers in SSA values", [] { - const std::array code{MoveV4One, MoveV5Two, MoveV6Four, MoveV1V5, EndProgram}; + const std::array code{MoveV4One, MoveV5Two, MoveV6Four, MoveV1V5, 0xe0700000u, 0x80000100u, EndProgram}; const auto shape = Scan(RecompileProgram(code)); return shape.functionArrays == 0 && shape.accessChains == 0; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp index 0d919f5d1..ae310f425 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp @@ -13,7 +13,7 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -constexpr std::uint32_t Extent = 16u; +constexpr std::uint32_t Extent = 64u; alignas(256) std::array pixels{}; alignas(256) std::array output{}; alignas(256) constexpr std::array, 6> positions{{ @@ -41,7 +41,7 @@ constexpr std::array computeCode{ std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { const auto address = reinterpret_cast(data); - return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu}; } void Draw(AgcDriver::VulkanDevice& device, bool relativeVertex) { @@ -112,7 +112,8 @@ void Dispatch(AgcDriver::VulkanDevice& device) { device.Dispatch(result, 1u, 1u, 1u); device.WaitIdle(); for (std::size_t index = 0; index < output.size(); ++index) { - Require(output[index] == index / 8u, "relative stage input dispatch did not preserve the implicit Z input at output " + std::to_string(index)); + Require(output[index] == index / 8u, "relative stage input dispatch did not preserve the implicit Z input at output " + std::to_string(index) + + ": got " + std::to_string(output[index]) + ", expected " + std::to_string(index / 8u)); } } From 5ec6cff3fae85661c2f69048a35c857b4e8c7826 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:05:00 -0400 Subject: [PATCH 158/318] fix(recompiler): include implicit inputs in relative vector windows --- .../Translation/src/InstructionTranslator.cpp | 91 ++++++++++++------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index f99b87af0..9d54fe93f 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -90,7 +90,8 @@ bool isCodeTableLoad(const ControlFlowGraph& cfg, std::uint32_t programCounter) return std::find(cfg.codeTableLoadProgramCounters.begin(), cfg.codeTableLoadProgramCounters.end(), programCounter) != cfg.codeTableLoadProgramCounters.end(); } -void translateExternalFetch(TranslationContext& context, const RdnaInstruction& call, const ShaderVertexInputInfo* input) { +template +void forEachExternalFetchLoad(const RdnaInstruction& call, const ShaderVertexInputInfo* input, TAction action) { if (input == nullptr || input->resourcesNum <= 0) { throw std::runtime_error("s_swappc_b64 fetch-shader call at program counter " + std::to_string(call.programCounter) + " has no parsed fetch-shader plan to inline"); } @@ -108,8 +109,9 @@ void translateExternalFetch(TranslationContext& context, const RdnaInstruction& fetch.programCounter = call.programCounter; fetch.destination.kind = RdnaOperandKind::VectorRegister; fetch.destination.reg = static_cast(destination.registerStart); + fetch.dataDwordCount = static_cast(destination.registersNum); fetch.formatted = true; - context.TranslateEmbeddedFetch(fetch, static_cast(index), static_cast(destination.registersNum)); + action(static_cast(index), fetch); } } @@ -161,6 +163,15 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrBuilder entryIr(program); entryIr.SetInsertionPoint(entryBlock); + const auto setVector = [&entryIr, &program](VectorReg reg, IrValue& value) { + auto& metadata = program.Metadata(); + const auto index = RegIndex(reg); + if (metadata.vectorWindowSize != 0u && index >= metadata.vectorWindowBase) { + metadata.vectorWindowSize = std::max(metadata.vectorWindowSize, index - metadata.vectorWindowBase + 1u); + } + entryIr.SetVectorReg(reg, value); + }; + const auto builtin = [&entryIr](StageInputKind kind, std::uint32_t component = 0u) -> IrValue& { return entryIr.Emit(IrOpcode::GetBuiltin, IrOpcodeType(IrOpcode::GetBuiltin), {&entryIr.Constant(static_cast(kind)), &entryIr.Constant(component)}); }; @@ -207,7 +218,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto* cs = options.inputInfo.compute; const std::uint32_t threadIds = cs->threadIdsNum > 0 ? std::min(static_cast(cs->threadIdsNum), 3u) : 0u; for (std::uint32_t index = 0; index < threadIds; index++) { - entryIr.SetVectorReg(static_cast(index), builtin(StageInputKind::LocalInvocationId, index)); + setVector(static_cast(index), builtin(StageInputKind::LocalInvocationId, index)); } std::uint32_t regOffset = 0; for (std::uint32_t index = 0; index < 3u; index++) { @@ -279,11 +290,11 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrValue& first = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(1u)), item) : unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, parity), item)); IrValue& second = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item) : size >= 2u ? unlessRestarted(entryIr.IMul(entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity), item)) : u32(0u); IrValue& third = fan ? u32(0u) : size == 3u ? unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item)) : u32(0u); - entryIr.SetVectorReg(static_cast(0), entryIr.BitwiseOr(entryIr.BitwiseAnd(first, u32(0xffffu)), entryIr.ShiftLeftLogical(second, u32(16u)))); - entryIr.SetVectorReg(static_cast(1), entryIr.BitwiseAnd(third, u32(0xffffu))); - entryIr.SetVectorReg(static_cast(2), entryIr.IAdd(firstPrimitive, local)); - entryIr.SetVectorReg(static_cast(3), u32(0u)); - entryIr.SetVectorReg(static_cast(4), u32(0u)); + setVector(static_cast(0), entryIr.BitwiseOr(entryIr.BitwiseAnd(first, u32(0xffffu)), entryIr.ShiftLeftLogical(second, u32(16u)))); + setVector(static_cast(1), entryIr.BitwiseAnd(third, u32(0xffffu))); + setVector(static_cast(2), entryIr.IAdd(firstPrimitive, local)); + setVector(static_cast(3), u32(0u)); + setVector(static_cast(4), u32(0u)); if (options.userDataBaseRegister != 0u || options.userDataCount < 8u) { throw std::runtime_error("mesh shader translation requires the merged program's eight hidden user words"); } @@ -301,30 +312,30 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO } IrValue& restartIndex = entryIr.Select(entryIr.IEqual(indexBytes, u32(2u)), u32(0xffffu), u32(0xffffffffu)); IrValue& fetchedIndex = entryIr.Select(entryIr.LogicalAnd(restartTable, entryIr.IEqual(index, restartIndex)), u32(0u), index); - entryIr.SetVectorReg(static_cast(5), entryIr.IAdd(draw(1u), entryIr.Select(indexed, fetchedIndex, inputVertex))); - entryIr.SetVectorReg(static_cast(6), u32(0u)); - entryIr.SetVectorReg(static_cast(7), u32(0u)); - entryIr.SetVectorReg(static_cast(8), entryIr.IAdd(draw(2u), builtin(StageInputKind::WorkgroupId, 1u))); + setVector(static_cast(5), entryIr.IAdd(draw(1u), entryIr.Select(indexed, fetchedIndex, inputVertex))); + setVector(static_cast(6), u32(0u)); + setVector(static_cast(7), u32(0u)); + setVector(static_cast(8), entryIr.IAdd(draw(2u), builtin(StageInputKind::WorkgroupId, 1u))); } else if (options.stage == ShaderStageKind::Local) { entryIr.SetScalarReg(static_cast(3), entryIr.Constant(64u)); - entryIr.SetVectorReg(static_cast(2), drawIndex(StageInputKind::VertexIndex)); - entryIr.SetVectorReg(static_cast(3), tessellationBase(TessellationBaseKind::RelativeIndex)); - entryIr.SetVectorReg(static_cast(5), drawIndex(StageInputKind::InstanceIndex)); + setVector(static_cast(2), drawIndex(StageInputKind::VertexIndex)); + setVector(static_cast(3), tessellationBase(TessellationBaseKind::RelativeIndex)); + setVector(static_cast(5), drawIndex(StageInputKind::InstanceIndex)); } else if (options.stage == ShaderStageKind::TessellationControl) { const auto& tess = options.inputInfo.vertex->tess; entryIr.SetScalarReg(static_cast(2), tessellationBase(TessellationBaseKind::OffChip)); entryIr.SetScalarReg(static_cast(4), tessellationBase(TessellationBaseKind::Factor)); entryIr.SetScalarReg(static_cast(3), entryIr.Constant(0x81010000u | tess.inputControlPoints | (tess.outputControlPoints << 8u))); - entryIr.SetVectorReg(static_cast(0), builtin(StageInputKind::PrimitiveId)); - entryIr.SetVectorReg(static_cast(1), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(builtin(StageInputKind::InvocationId), entryIr.Constant(8u)), tessellationBase(TessellationBaseKind::RelativeIndex))); + setVector(static_cast(0), builtin(StageInputKind::PrimitiveId)); + setVector(static_cast(1), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(builtin(StageInputKind::InvocationId), entryIr.Constant(8u)), tessellationBase(TessellationBaseKind::RelativeIndex))); } else if (options.stage == ShaderStageKind::TessellationEvaluation) { entryIr.SetScalarReg(static_cast(3), entryIr.Constant(64u)); entryIr.SetScalarReg(static_cast(4), tessellationBase(TessellationBaseKind::OffChip)); - entryIr.SetVectorReg(static_cast(0), tessellationBase(TessellationBaseKind::PassthroughPrimitive)); - entryIr.SetVectorReg(static_cast(5), builtin(StageInputKind::TessCoord, 0u)); - entryIr.SetVectorReg(static_cast(6), builtin(StageInputKind::TessCoord, 1u)); - entryIr.SetVectorReg(static_cast(7), tessellationBase(TessellationBaseKind::RelativeIndex)); - entryIr.SetVectorReg(static_cast(8), builtin(StageInputKind::PrimitiveId)); + setVector(static_cast(0), tessellationBase(TessellationBaseKind::PassthroughPrimitive)); + setVector(static_cast(5), builtin(StageInputKind::TessCoord, 0u)); + setVector(static_cast(6), builtin(StageInputKind::TessCoord, 1u)); + setVector(static_cast(7), tessellationBase(TessellationBaseKind::RelativeIndex)); + setVector(static_cast(8), builtin(StageInputKind::PrimitiveId)); } else if (options.stage == ShaderStageKind::Pixel) { const auto* ps = options.inputInfo.pixel; const auto vgpr = [&](PixelInput input) { return ps->psInputVgpr[static_cast(input)]; }; @@ -332,44 +343,44 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO for (const auto [input, kind] : {std::pair{PixelInput::PerspectiveCenter, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::PerspectiveCentroid, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::LinearCenter, StageInputKind::BaryCoordNoPerspective}, std::pair{PixelInput::LinearCentroid, StageInputKind::BaryCoordNoPerspective}}) { if (!loaded(input)) continue; - entryIr.SetVectorReg(static_cast(vgpr(input)), builtin(kind, 0u)); - entryIr.SetVectorReg(static_cast(vgpr(input) + 1u), builtin(kind, 1u)); + setVector(static_cast(vgpr(input)), builtin(kind, 0u)); + setVector(static_cast(vgpr(input) + 1u), builtin(kind, 1u)); } if (loaded(PixelInput::PositionX)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionX)), builtin(StageInputKind::FragCoord, 0u)); + setVector(static_cast(vgpr(PixelInput::PositionX)), builtin(StageInputKind::FragCoord, 0u)); } if (loaded(PixelInput::PositionY)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionY)), builtin(StageInputKind::FragCoord, 1u)); + setVector(static_cast(vgpr(PixelInput::PositionY)), builtin(StageInputKind::FragCoord, 1u)); } if (loaded(PixelInput::PositionZ)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionZ)), builtin(StageInputKind::FragCoord, 2u)); + setVector(static_cast(vgpr(PixelInput::PositionZ)), builtin(StageInputKind::FragCoord, 2u)); } if (loaded(PixelInput::PositionW)) { IrValue& reciprocalW = entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 3u)); IrValue& w = entryIr.Emit(IrOpcode::FPRecip32, IrOpcodeType(IrOpcode::FPRecip32), {&reciprocalW}); - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); + setVector(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); } if (loaded(PixelInput::FrontFace)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::FrontFace)), builtin(StageInputKind::FrontFacing)); + setVector(static_cast(vgpr(PixelInput::FrontFace)), builtin(StageInputKind::FrontFacing)); } if (loaded(PixelInput::Ancillary)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::Ancillary)), builtin(StageInputKind::PackedAncillary)); + setVector(static_cast(vgpr(PixelInput::Ancillary)), builtin(StageInputKind::PackedAncillary)); } if (loaded(PixelInput::LineStipple)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::LineStipple)), entryIr.Constant(0u)); + setVector(static_cast(vgpr(PixelInput::LineStipple)), entryIr.Constant(0u)); } if (loaded(PixelInput::PositionFixedPoint)) { IrValue& x = entryIr.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 0u))}); IrValue& y = entryIr.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 1u))}); - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionFixedPoint)), entryIr.BitwiseOr(entryIr.BitwiseAnd(x, entryIr.Constant(0xffffu)), entryIr.ShiftLeftLogical(y, entryIr.Constant(16u)))); + setVector(static_cast(vgpr(PixelInput::PositionFixedPoint)), entryIr.BitwiseOr(entryIr.BitwiseAnd(x, entryIr.Constant(0xffffu)), entryIr.ShiftLeftLogical(y, entryIr.Constant(16u)))); } } else if (options.stage == ShaderStageKind::Vertex) { if (options.userDataBaseRegister >= 8u) { const std::uint32_t lanes = options.waveSize == 64u && options.inputInfo.vertex->hostSubgroupSize == 32u ? 32u : options.waveSize; entryIr.SetScalarReg(static_cast(3), entryIr.Constant(lanes | (lanes << 8u))); } - entryIr.SetVectorReg(static_cast(5), drawIndex(StageInputKind::VertexIndex)); - entryIr.SetVectorReg(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); + setVector(static_cast(5), drawIndex(StageInputKind::VertexIndex)); + setVector(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); } } @@ -392,6 +403,11 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con continue; } includeInstructionVectorRegisters(instruction, vectorLimit); + if (cfg.hasFetchCall && instruction.programCounter == cfg.fetchCallProgramCounter) { + forEachExternalFetchLoad(instruction, options.inputInfo.vertex, [&](std::uint32_t, const RdnaInstruction& fetch) { + vectorLimit = std::min(NumVectorRegs, std::max(vectorLimit, fetch.destination.reg + fetch.dataDwordCount)); + }); + } } } @@ -482,6 +498,9 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con blocks.front()->AddBranch(blocks[blockIndices.at(cfg.blocks.front().id)]); emitEntryPrologue(program, *blocks.front(), options); + if (program.Metadata().vectorWindowSize != 0u) { + vectorLimit = program.Metadata().vectorWindowBase + program.Metadata().vectorWindowSize; + } for (const auto& cfgBlock : cfg.blocks) { const auto typedIndex = blockIndices.at(cfgBlock.id); @@ -511,7 +530,9 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con } context.TranslateInstruction(instruction); if (cfg.hasFetchCall && instruction.programCounter == cfg.fetchCallProgramCounter) { - translateExternalFetch(context, instruction, options.inputInfo.vertex); + forEachExternalFetchLoad(instruction, options.inputInfo.vertex, [&](std::uint32_t attribute, const RdnaInstruction& fetch) { + context.TranslateEmbeddedFetch(fetch, attribute, fetch.dataDwordCount); + }); } } context.AddBranchCondition(cfgBlock, blockInfos[typedIndex]); From 22827bf6ad532c012cb7d39e002af7ea72dc4ac1 Mon Sep 17 00:00:00 2001 From: jdmagic Date: Fri, 9 Oct 2026 13:50:06 -0400 Subject: [PATCH 159/318] fix(libSceNet): sceNetEpollControl takes 2 as MOD and 3 as DEL sceNetEpollControl used Linux's EPOLL_CTL numbering (1 ADD, 2 DEL, 3 MOD). The SCE values are 1 ADD, 2 MOD, 3 DEL (shadPS4 net_types.h, ORBIS_NET_EPOLL_CTL_*). A title removing a socket with op 3 and a null event got EINVAL, and op 2 deleted the registration it meant to modify. PPSA01289 (Sackboy: A Big Adventure) removes its UDP socket from its epoll this way before closing it, every time its network session is torn down. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 13 ++++++++----- core/libs/tests/GuestSceNet.cpp | 6 ++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 3bfaa1229..06ce53847 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -70,6 +70,9 @@ constexpr int NET_AF_INET6 = 28; constexpr int NET_SOCK_STREAM = 1; constexpr int NET_SOCK_DGRAM = 2; constexpr int NET_SOCK_RAW = 3; +constexpr int NET_EPOLL_CTL_ADD = 1; +constexpr int NET_EPOLL_CTL_MOD = 2; +constexpr int NET_EPOLL_CTL_DEL = 3; constexpr int NET_SOL_SOCKET = 0xFFFF; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; @@ -958,13 +961,13 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e if (g_epolls.count(eid) == 0) { return fail(NET_EBADF); } - if (op < 1 || op > 3) return fail(NET_EINVAL); - if (op != 2 && !event) return fail(NET_EINVAL); - if (op != 2 && g_socks.count(id) == 0) return fail(NET_EBADF); + if (op != NET_EPOLL_CTL_ADD && op != NET_EPOLL_CTL_MOD && op != NET_EPOLL_CTL_DEL) return fail(NET_EINVAL); + if (op != NET_EPOLL_CTL_DEL && !event) return fail(NET_EINVAL); + if (op != NET_EPOLL_CTL_DEL && g_socks.count(id) == 0) return fail(NET_EBADF); auto& registrations = g_epoll_socks[eid]; - if (op == 1) { + if (op == NET_EPOLL_CTL_ADD) { if (!registrations.emplace(id, *event).second) return fail(NET_EADDRINUSE); - } else if (op == 2) { + } else if (op == NET_EPOLL_CTL_DEL) { if (registrations.erase(id) == 0) return fail(NET_ENOENT); } else { auto registration = registrations.find(id); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 1579e6645..6c82f8ea1 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -400,6 +400,12 @@ int main() { Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1 && (ready.events & 1) && ready.ident == static_cast(accepted)); Require(sceNetShutdown(accepted, 1) == 0); Require(Failed(sceNetSend(accepted, request, sizeof(request), 0), 32)); + registration.data.u32 = 78; + Require(sceNetEpollControl(epoll, 2, accepted, ®istration) == 0); + Require(Failed(sceNetEpollControl(epoll, 2, accepted, nullptr), 22)); + Require(sceNetEpollControl(epoll, 3, accepted, nullptr) == 0); + Require(Failed(sceNetEpollControl(epoll, 3, accepted, nullptr), 2)); + Require(Failed(sceNetEpollControl(epoll, 2, accepted, ®istration), 2)); Require(sceNetEpollDestroy(epoll) == 0); Require(sceNetSocketClose(accepted) == 0); bool send_failed = false; From 12b18af55052f2567bfc56032f809a9df431f22f Mon Sep 17 00:00:00 2001 From: jdmagic Date: Fri, 9 Oct 2026 14:28:46 -0400 Subject: [PATCH 160/318] fix(libkernel): close() closes libSceNet sockets A libSceNet socket is a kernel descriptor on the console, so titles close it with close(). libSceNet keeps its sockets in its own table, and libkernel's close passed their numbers to the CRT, which reported an invalid parameter and left the socket open. libSceNet now registers a closer with libkernel, as it registers its poller for select. PPSA01289 (Sackboy: A Big Adventure) closes its P2P socket this way each time it tears its network session down, leaking one open socket per attempt. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 13 +++++++++++++ core/libs/prx/libkernel/File/src/Stdio.cpp | 7 +++++++ .../prx/libkernel/Socket/include/SocketPoll.hpp | 3 +++ core/libs/prx/libkernel/Socket/src/Socket.cpp | 10 ++++++++++ core/libs/tests/GuestSceNet.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 39 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 3bfaa1229..798494847 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -439,6 +439,19 @@ int PollSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMillisec const bool g_socketPollerRegistered = (KernelSetSocketPoller_nid_no_patch(&PollSockets), true); +extern "C" int APS5_VABI sceNetSocketClose(int s); + +bool CloseSocketForKernel(int descriptor, int* error) { + { + std::lock_guard lk(g_mutex); + if (g_socks.count(descriptor) == 0) return false; + } + *error = sceNetSocketClose(descriptor) == 0 ? 0 : *errno_slot(); + return true; +} + +const bool g_socketCloserRegistered = (KernelSetSocketCloser_nid_no_patch(&CloseSocketForKernel), true); + int APS5_VABI sceNetInit_nid_postfix(void) { std::lock_guard lk(g_mutex); g_net_inited = true; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index fbe87450b..3e619ac9a 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -16,6 +16,7 @@ #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" #include #include #include @@ -466,6 +467,12 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { int APS5_VABI close_nid_postfix(int d) { if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d); + int socketError = 0; + if (KernelCloseSocket_nid_no_patch(d, &socketError)) { + if (socketError == 0) return 0; + *__error_nid_postfix() = socketError; + return -1; + } File::ForgetRandomDevice(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 71d5f62e7..41455ba03 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -16,6 +16,9 @@ struct Entry { }; using Poller = int (*)(Entry* entries, int count, int timeoutMilliseconds); +using Closer = bool (*)(int descriptor, int* error); } extern "C" void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller); +extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer closer); +extern "C" bool KernelCloseSocket_nid_no_patch(int descriptor, int* error); diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 3f71f4f1c..09baf528f 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -31,6 +31,7 @@ struct GuestTimeval { }; std::atomic g_socketPoller{nullptr}; +std::atomic g_socketCloser{nullptr}; bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; @@ -105,6 +106,15 @@ void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller) { g_socketPoller.store(poller); } +void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer closer) { + g_socketCloser.store(closer); +} + +bool KernelCloseSocket_nid_no_patch(int descriptor, int* error) { + const auto closer = g_socketCloser.load(); + return closer != nullptr && closer(descriptor, error); +} + int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout) { const auto* limit = static_cast(timeout); if (nfds < 0 || nfds > SelectDescriptorLimit || (limit != nullptr && (limit->seconds < 0 || limit->microseconds < 0 || limit->microseconds >= 1000000))) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 1579e6645..e4978db04 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -338,6 +338,11 @@ int main() { CheckLinkLocalAllNodesIpv6(); CheckUnspecifiedIpv6(); + const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); + Require(closed >= 0); + Require(close_nid_postfix(closed) == 0); + Require(Failed(sceNetSocketClose(closed), 9)); + const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..497f97eae 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -422,6 +422,7 @@ Throughout the project, every function at every stage either **does exactly what - [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported +- [close](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) closes a [libSceNet](../../core/libs/prx/libSceNet/Export.cpp) socket through a hook libSceNet registers, as `select` polls them. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [Content export sessions](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified - [Runtime-loaded modules](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. From 5d14cd02ffb91212aee184c0f3c85f55448396dd Mon Sep 17 00:00:00 2001 From: jdmagic Date: Sat, 10 Oct 2026 01:19:53 -0400 Subject: [PATCH 161/318] fix(libSceNet): failures also set libkernel's errno libSceNet stored its errno only in its own slot (sceNetErrnoLoc). Titles that use the BSD socket model read errno after a libSceNet call, as the console's libSceNet calls are kernel calls that set it; shadPS4 mirrors its net errno into the kernel errno the same way (SetKernelErrnoHook). PPSA01289 (Sackboy: A Big Adventure) reads errno through Unreal's GetLastErrorCode after sceNetRecvfrom. It never saw EWOULDBLOCK, so UIpNetDriver::TickDispatch kept receiving and the game thread never left its first networked frame. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 21 ++++++++++++++------- core/libs/tests/GuestSceNet.cpp | 2 ++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 3bfaa1229..e4c7f0188 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -301,8 +301,15 @@ int* errno_slot() { return &e; } -int fail(int err) { +extern "C" int* APS5_VABI __error_nid_postfix(); + +void set_errno(int err) { *errno_slot() = err; + *__error_nid_postfix() = err; +} + +int fail(int err) { + set_errno(err); return NET_ERROR_BASE | err; } @@ -1091,11 +1098,11 @@ int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_t size) { if (src == nullptr || dst == nullptr) { - *errno_slot() = NET_EINVAL; + set_errno(NET_EINVAL); return nullptr; } if (af != NET_AF_INET && af != NET_AF_INET6) { - *errno_slot() = NET_EAFNOSUPPORT; + set_errno(NET_EAFNOSUPPORT); return nullptr; } char text[INET6_ADDRSTRLEN]; @@ -1105,12 +1112,12 @@ const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_ const auto* result = ::inet_ntop(af == NET_AF_INET ? AF_INET : AF_INET6, src, text, sizeof(text)); #endif if (!result) { - *errno_slot() = native_error(); + set_errno(native_error()); return nullptr; } const auto length = std::strlen(text) + 1; if (length > size) { - *errno_slot() = NET_ENOSPC; + set_errno(NET_ENOSPC); return nullptr; } std::memcpy(dst, text, length); @@ -1163,7 +1170,7 @@ int lookup_ipv4(int rid, const char* hostname, const char* func, std::vector(&address), sizeof(address), hostname, static_cast(len), nullptr, 0, NI_NAMEREQD); if (result != 0) { - *errno_slot() = result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT; + set_errno(result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT); log_soft(__func__, "host reverse DNS lookup failed"); set_resolver_error(rid, NET_ERROR_RESOLVER_ENODNS); return NET_ERROR_RESOLVER_ENODNS; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 1579e6645..4e310dd9b 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -410,7 +410,9 @@ int main() { Require(send_failed); Require(sceNetSocketClose(client) == 0); Require(sceNetSocketClose(listener) == 0); + *__error_nid_postfix() = 0; Require(sceNetSocketClose(listener) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); + Require(*__error_nid_postfix() == 9); const int udp_receiver = sceNetSocket(nullptr, 2, 2, 17); const int udp_sender = sceNetSocket(nullptr, 2, 2, 17); From d5805fb3e415677d65d97be2da999dc1a48dc3a3 Mon Sep 17 00:00:00 2001 From: jdmagic Date: Sat, 10 Oct 2026 00:22:26 -0400 Subject: [PATCH 162/318] fix(libkernel): setsockopt sets options of libSceNet sockets A title creating a socket with sceNetSocket sets its options with the BSD setsockopt, which looked the descriptor up only among libkernel's own sockets and returned EBADF. libSceNet now registers an option setter with libkernel, as it does for close and select, and accepts SO_REUSEADDR, SO_BROADCAST, SO_SNDBUF and SO_RCVBUF like libkernel's own sockets. PPSA01289 (Sackboy: A Big Adventure) sets SO_BROADCAST on its UDP P2P socket this way in UIpNetDriver::InitBase. The failure made every listen fail, and the engine travelled back to its default map with ?closed, reloading it about every two seconds behind the loading screen. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 21 +++++++++++++++++++ .../libkernel/Socket/include/SocketPoll.hpp | 3 +++ core/libs/prx/libkernel/Socket/src/Socket.cpp | 10 +++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 +++++ core/libs/tests/GuestSceNet.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 2 +- 6 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 798494847..c2b9212f8 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -74,6 +74,10 @@ constexpr int NET_SOL_SOCKET = 0xFFFF; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; constexpr int NET_SO_NBIO = 0x1200; +constexpr int NET_SO_REUSEADDR = 0x4; +constexpr int NET_SO_BROADCAST = 0x20; +constexpr int NET_SO_SNDBUF = 0x1001; +constexpr int NET_SO_RCVBUF = 0x1002; constexpr int NET_MSG_PEEK = 0x2; constexpr int NET_MSG_TRUNC = 0x10; constexpr int NET_UIO_MAXIOV = 1024; @@ -452,6 +456,19 @@ bool CloseSocketForKernel(int descriptor, int* error) { const bool g_socketCloserRegistered = (KernelSetSocketCloser_nid_no_patch(&CloseSocketForKernel), true); +extern "C" int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen); + +bool SetSocketOptionForKernel(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error) { + { + std::lock_guard lk(g_mutex); + if (g_socks.count(descriptor) == 0) return false; + } + *error = sceNetSetsockopt(descriptor, level, option, value, length) == 0 ? 0 : *errno_slot(); + return true; +} + +const bool g_socketOptionSetterRegistered = (KernelSetSocketOptionSetter_nid_no_patch(&SetSocketOptionForKernel), true); + int APS5_VABI sceNetInit_nid_postfix(void) { std::lock_guard lk(g_mutex); g_net_inited = true; @@ -844,6 +861,10 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, &timeout, sizeof(timeout)) != 0) return fail(native_error()); #endif + } else if (optname == NET_SO_REUSEADDR || optname == NET_SO_BROADCAST || optname == NET_SO_SNDBUF || optname == NET_SO_RCVBUF) { + const int native_option = optname == NET_SO_REUSEADDR ? SO_REUSEADDR : optname == NET_SO_BROADCAST ? SO_BROADCAST : optname == NET_SO_SNDBUF ? SO_SNDBUF : SO_RCVBUF; + if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, reinterpret_cast(&value), sizeof(value)) != 0) + return fail(native_error()); } else { return fail(NET_EOPNOTSUPP); } diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 41455ba03..c990c6686 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -17,8 +17,11 @@ struct Entry { using Poller = int (*)(Entry* entries, int count, int timeoutMilliseconds); using Closer = bool (*)(int descriptor, int* error); +using OptionSetter = bool (*)(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); } extern "C" void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller); extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer closer); extern "C" bool KernelCloseSocket_nid_no_patch(int descriptor, int* error); +extern "C" void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter setter); +extern "C" bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 09baf528f..441cb62ed 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -32,6 +32,7 @@ struct GuestTimeval { std::atomic g_socketPoller{nullptr}; std::atomic g_socketCloser{nullptr}; +std::atomic g_socketOptionSetter{nullptr}; bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; @@ -115,6 +116,15 @@ bool KernelCloseSocket_nid_no_patch(int descriptor, int* error) { return closer != nullptr && closer(descriptor, error); } +void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter setter) { + g_socketOptionSetter.store(setter); +} + +bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error) { + const auto setter = g_socketOptionSetter.load(); + return setter != nullptr && setter(descriptor, level, option, value, length, error); +} + int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout) { const auto* limit = static_cast(timeout); if (nfds < 0 || nfds > SelectDescriptorLimit || (limit != nullptr && (limit->seconds < 0 || limit->microseconds < 0 || limit->microseconds >= 1000000))) { diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 52f37a428..14b397b41 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -18,6 +18,7 @@ #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" #include #include #include @@ -463,6 +464,10 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { } int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { + int netError = 0; + if (descriptor < GuestSockets::FirstDescriptor && KernelSetSocketOption_nid_no_patch(descriptor, level, option, value, length, &netError)) { + return netError == 0 ? 0 : Fail(netError); + } const auto socket = Lookup(descriptor); if (!socket) return -1; const int nativeOption = Option(option); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e4978db04..9dd1f499a 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -56,6 +56,7 @@ int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceNetInetPton(int, const char*, void*); const char* APS5_VABI sceNetInetNtop(int, const void*, char*, std::uint32_t); @@ -340,6 +341,10 @@ int main() { const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); + const int enabled = 1; + Require(setsockopt_nid_postfix(closed, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); + Require(sceNetSetsockopt(closed, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); + Require(sceNetSetsockopt(closed, 0xffff, 0x4, &enabled, sizeof(enabled)) == 0); Require(close_nid_postfix(closed) == 0); Require(Failed(sceNetSocketClose(closed), 9)); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 497f97eae..d31552ae4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -422,7 +422,7 @@ Throughout the project, every function at every stage either **does exactly what - [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported -- [close](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) closes a [libSceNet](../../core/libs/prx/libSceNet/Export.cpp) socket through a hook libSceNet registers, as `select` polls them. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space +- [close](../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [Content export sessions](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified - [Runtime-loaded modules](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. From 2fd05f47e200e869d5f83eb8b1e6a169ad2cf9a1 Mon Sep 17 00:00:00 2001 From: Simon <340958984+ee6aba54@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:30:54 +0200 Subject: [PATCH 163/318] feat(libSceNpWebApi2): export sceNpWebApi2AddWebTraceTag PPSA33016 imports sceNpWebApi2AddWebTraceTag (NID MgsTa76wlEk), and no library exported it, so the loader stopped before the title started. The title does not call it at start-up or in play, so the export throws like the other unimplemented functions. --- core/libs/prx/libSceNpWebApi2/Export.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 75c959187..806accdb3 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -36,6 +36,11 @@ int APS5_VABI sceNpWebApi2AddHttpRequestHeader(int64_t request_id, const char* f return 0; } +int APS5_VABI sceNpWebApi2AddWebTraceTag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + void APS5_VABI sceNpWebApi2CheckTimeout(void) { } From 4680a52c1edf0c3722a1cb5fcd858e5151689eaa Mon Sep 17 00:00:00 2001 From: PrintOrb Date: Sun, 11 Oct 2026 18:29:42 -0500 Subject: [PATCH 164/318] fix(libkernel): return positioned descriptor errors without CRT reports --- core/libs/prx/libkernel/CMakeLists.txt | 3 ++ core/libs/prx/libkernel/File/src/Stdio.cpp | 18 ++++---- core/libs/tests/GuestAio.cpp | 51 +++++++++++++++++++++- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a85c9ecaa..e97c62a49 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -252,6 +252,9 @@ if(BUILD_TESTING) configure_windows_unwind(guest_aio_tests) add_test(NAME guest_aio COMMAND guest_aio_tests) if(WIN32) + add_test(NAME guest_aio_bad_descriptors COMMAND guest_aio_tests bad-descriptors) + set_tests_properties(guest_aio_bad_descriptors PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_aio PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index fbe87450b..4f696ddf9 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -46,6 +46,14 @@ static constexpr std::uint64_t KERNEL_MNT_NOSUID = 0x8; #include #include #include "prx/libkernel/File/include/WindowsFileTime.hpp" +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +static void IgnoreDescriptorParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +template static auto WithoutParameterHandler(TCall call) { + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreDescriptorParameter); + const auto result = call(); + _set_thread_local_invalid_parameter_handler(previous); + return result; +} static int NativeRmdir(const std::filesystem::path& path) { return ::_wrmdir(path.wstring().c_str()); } @@ -164,7 +172,7 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); if (result != -2) return result; } - const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); if (handle == INVALID_HANDLE_VALUE) { errno = EBADF; return -1; @@ -420,14 +428,6 @@ static int PathError(const char* path) { } #ifdef _WIN32 -extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); -static void IgnoreDescriptorParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} -template static int WithoutParameterHandler(TCall call) { - const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreDescriptorParameter); - const int result = call(); - _set_thread_local_invalid_parameter_handler(previous); - return result; -} bool DescriptorIsOpen_nid_no_patch(int descriptor) { return descriptor >= 0 && WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor) == -1 ? -1 : 0; }) == 0; } diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index e1a51c74e..96d803063 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -15,9 +15,15 @@ #include #include #include +#ifdef _WIN32 +#include +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#endif extern "C" { int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); int APS5_VABI sceKernelClose(int d); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret); int APS5_VABI sceKernelAioInitializeImpl(void* param, std::int32_t size); @@ -42,7 +48,50 @@ static void Require(bool value, int line) { } } #define Check(value) Require((value), __LINE__) -int main() { +#ifdef _WIN32 +static int g_parameterReports = 0; +static void CountParameterReport(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) { + ++g_parameterReports; +} +static void CheckBadPositionedDescriptors() { + const auto file = std::filesystem::path("anyps5-aio-bad-descriptor-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + { std::ofstream stream(file, std::ios::binary); stream << "data"; Check(static_cast(stream)); } + const int closed = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Check(closed >= 0 && sceKernelClose(closed) == 0); + const auto previous = _set_thread_local_invalid_parameter_handler(CountParameterReport); + for (int descriptor : {-1, closed}) { + for (std::size_t count : {std::size_t{0}, std::size_t{1}}) { + char buffer = 'x'; + Check(sceKernelPread(descriptor, &buffer, count, 0) == SCE_KERNEL_ERROR_EBADF); + Check(sceKernelPwrite(descriptor, &buffer, count, 0) == SCE_KERNEL_ERROR_EBADF); + for (bool write : {false, true}) { + KernelAioResult result{-1, 0}; + KernelAioRwRequest request{0, count, &buffer, &result, descriptor}; + std::int32_t id = 0; + Check((write ? sceKernelAioSubmitWriteCommands(&request, 1, 0, &id) + : sceKernelAioSubmitReadCommands(&request, 1, 0, &id)) == 0); + Check(result.state == 4 && result.return_value == SCE_KERNEL_ERROR_EBADF); + std::int32_t removed = -1; + Check(sceKernelAioDeleteRequest(id, &removed) == 0); + } + Check(buffer == 'x'); + Check(g_parameterReports == 0); + } + } + Check(::_get_osfhandle(closed) == -1); + Check(g_parameterReports == 1); + Check(_set_thread_local_invalid_parameter_handler(previous) == CountParameterReport); + Check(std::filesystem::remove(file)); +} +#endif +int main(int argc, char** argv) { +#ifdef _WIN32 + if (argc > 1 && std::strcmp(argv[1], "bad-descriptors") == 0) { + CheckBadPositionedDescriptors(); + return 0; + } +#endif const auto root = std::filesystem::path("anyps5-aio-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Check(std::filesystem::create_directory(root)); From 78abb2b46a2ddfa775225b8cce2ff2e1d3591b60 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 20:35:58 -0300 Subject: [PATCH 165/318] fix(libSceUserService): report the console's default accessibility settings On a PS5 (firmware 13.60) a user with every accessibility setting at its default reads chat transcription 0, press-and-hold delay 0, trigger effect 1, vibration 1, zoom 0 and zoom follow focus 1; all six read 0 here. A user who is not logged in returns 0x80960009 with or without an output, as sceUserServiceGetAgeLevel does. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceUserService/Export.cpp | 47 ++++++++-------------- core/libs/tests/GuestUserService.cpp | 30 ++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 07190caa3..8331ab7d1 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -6,54 +6,41 @@ #include #include "prx/libSceUserService/UserService.hpp" +static int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t value) { + if (user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_NOT_LOGGED_IN; + } + if (setting == nullptr) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *setting = value; + return USER_SERVICE_OK; +} + extern "C" { int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32_t* chat_transcription) { - if (chat_transcription == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *chat_transcription = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, chat_transcription, 0); } int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hold_delay) { - if (press_and_hold_delay == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *press_and_hold_delay = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, press_and_hold_delay, 0); } int APS5_VABI sceUserServiceGetAccessibilityTriggerEffect(int user_id, int32_t* trigger_effect) { - if (trigger_effect == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *trigger_effect = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, trigger_effect, 1); } int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, int32_t* vibration) { - if (vibration == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *vibration = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, vibration, 1); } int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, int32_t* zoom_enabled) { - if (zoom_enabled == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *zoom_enabled = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, zoom_enabled, 0); } int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, int32_t* zoom_follow_focus) { - if (zoom_follow_focus == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *zoom_follow_focus = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, zoom_follow_focus, 1); } int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 1931f036f..83d7afc73 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -9,6 +9,12 @@ int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdL int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id); int APS5_VABI sceUserServiceGetAgeLevel(int user_id, std::uint32_t* age_level); +int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityTriggerEffect(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, std::int32_t* value); } namespace { @@ -53,4 +59,28 @@ int main() { } Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID, &age_level) == USER_SERVICE_OK); Require(age_level == 0xffffffffu); + + struct Setting { + int (APS5_VABI *get)(int, std::int32_t*); + std::int32_t value; + }; + const Setting settings[] = { + {sceUserServiceGetAccessibilityChatTranscription, 0}, + {sceUserServiceGetAccessibilityPressAndHoldDelay, 0}, + {sceUserServiceGetAccessibilityTriggerEffect, 1}, + {sceUserServiceGetAccessibilityVibration, 1}, + {sceUserServiceGetAccessibilityZoomEnabled, 0}, + {sceUserServiceGetAccessibilityZoomFollowFocus, 1}, + }; + for (const Setting& setting : settings) { + std::int32_t value = 0x5a5a5a5a; + Require(setting.get(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + for (int user_id : absent_users) { + Require(setting.get(user_id, &value) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(value == 0x5a5a5a5a); + Require(setting.get(user_id, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } + Require(setting.get(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK); + Require(value == setting.value); + } } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6999b4c00..e09ae367d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -275,6 +275,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNpSessionSignalingGetMemoryInfo](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile +- [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output - [sceGameUpdateGetAddcontLatestVersion](../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw - [sceSystemServiceParamGetInt](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed From 45fcf734ab0be5ddfdc2576829b5036a3340a46a Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:31:27 +0200 Subject: [PATCH 166/318] fix(libSceAgc): emit NOP packets for the DCB workload calls sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete threw. They now write a plain two-dword NOP and keep no workload state. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgc/DcbState/src/Workload.cpp | 22 ++++++++++++++----- core/libs/prx/libSceAgc/tests/Commands.cpp | 15 +++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index 2ce29cfe2..c2db4e988 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -6,23 +6,33 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr std::uint32_t OpcodeNop = 0x10; + +uint32_t* EmitNop(CommandBuffer* buf, const char* function) { + Agc::Command::Require(buf != nullptr, function, "null command buffer"); + auto* packet = Agc::Command::Allocate(buf, 2, function); + packet[0] = Agc::Command::Header(OpcodeNop, 2, 0); + packet[1] = 0; + return packet; +} + +} + extern "C" { uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { - (void)buf; (void)stream_id; (void)workload_id; - NotImplemented_nid_no_patch(__func__); - return nullptr; + return EmitNop(buf, __func__); } uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - (void)buf; (void)stream_id; (void)workload_ids; (void)workload_count; - NotImplemented_nid_no_patch(__func__); - return nullptr; + return EmitNop(buf, __func__); } } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index d3d9c7f83..edb09cbac 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -29,6 +29,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuff extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation); extern "C" std::uint32_t* APS5_VABI sceAgcDcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcDcbPopMarker(CommandBuffer* buf); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t stream_id, std::uint32_t workload_id); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t stream_id, const std::uint32_t* workload_ids, std::uint32_t workload_count); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); @@ -207,6 +209,18 @@ void testMarkers() { expectFailure([] { sceAgcAcbSetMarker(nullptr, "frame", 0); }); } +void testWorkloads() { + Storage dcb; + const std::uint32_t ids[2] = {3, 4}; + const auto* active = sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2); + check(active == dcb.words.data() && active[0] == Agc::Command::Header(0x10, 2, 0) && active[1] == 0, "workloads active packet is not a plain NOP"); + const auto* complete = sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3); + check(complete == dcb.words.data() + 2 && complete[0] == Agc::Command::Header(0x10, 2, 0) && complete[1] == 0, "workload complete packet is not a plain NOP"); + check(dcb.buffer.cursor_up == dcb.words.data() + 4, "incorrect workload cursor advance"); + expectFailure([] { sceAgcDcbSetWorkloadsActive(nullptr, 1, nullptr, 0); }); + expectFailure([] { sceAgcDcbSetWorkloadComplete(nullptr, 1, 3); }); +} + void testIndexBuffer() { Storage storage; alignas(4) std::uint16_t indices[2]{}; @@ -547,6 +561,7 @@ int main(int argc, char** argv) { testClearState(); testIndexedIndirectDraws(); testMarkers(); + testWorkloads(); testIndexBuffer(); testIndirectBase(); testContextState(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..fd97d1e42 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -659,6 +659,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceQoSEnd](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; no endpoint is implemented - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - emit a plain two-dword NOP packet and keep no workload state: the stream and workload ids are not recorded, so a capture tool never sees them. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. The packet the console writes is unknown. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws - [pthread_cond_reltimedwait_np](../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console From c35d05395ddc3ffa160f62fefc1c31a262205051 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 23:37:17 +0000 Subject: [PATCH 167/318] fix(libc): reject 16-bit wide conversions in sscanf_s and after the ' flag on Linux Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libc/src/Formatting.cpp | 10 +++++++++- core/libs/tests/Formatting.cpp | 9 ++++++++- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 76d5c01a1..7982d342a 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -41,9 +41,15 @@ void RejectGuestWideScan(const char* format) { while ((format = std::strchr(format, '%')) != nullptr) { ++format; if (*format == '%') { ++format; continue; } - while (*format && std::strchr("*0123456789$", *format)) ++format; + while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format; if (*format == 'S' || *format == 'C' || (*format == 'l' && format[1] != '\0' && std::strchr("sc[", format[1]))) NotImplemented_nid_no_patch("scanf 16-bit wide conversion on Linux"); + if (*format == '[') { + ++format; + if (*format == '^') ++format; + if (*format == ']') ++format; + while (*format && *format != ']') ++format; + } if (*format) ++format; } } @@ -373,6 +379,7 @@ int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) #else int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) { + RejectGuestWideScan(format); std::va_list args; va_start(args, format); const int result = ScanGuest(buffer, format, true, [&] { return va_arg(args, void*); }, [&] { return va_arg(args, unsigned int); }); @@ -451,6 +458,7 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { #else int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaList* args) { + RejectGuestWideScan(format); LibcDetail::FormatArguments arguments(args); return ScanGuest(buffer, format, true, [&] { return arguments.Next(); }, [&] { return arguments.Next(); }); } diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index ca9bd4f3f..2e1344ca4 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -185,7 +185,14 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { try { sscanf_nid_postfix("ab cd", format, wide); } catch (const std::runtime_error&) { return wide[0] == u'!'; } return false; }; - Require(rejects("%ls") && rejects("%*s %3lc") && rejects("%l[a-z]") && rejects("%S") && rejects("%2$C")); + Require(rejects("%ls") && rejects("%*s %3lc") && rejects("%l[a-z]") && rejects("%S") && rejects("%2$C") && rejects("%'ls")); + const auto secureRejects = [&](const char* format) { + try { ScanListS("ab cd", format, wide, 4u); } catch (const std::runtime_error&) { return wide[0] == u'!'; } + return false; + }; + Require(secureRejects("%ls") && secureRejects("%lc")); + char narrow[8] = {}; + Require(sscanf_nid_postfix("ab%", "%7[^%ls]", narrow) == 1 && std::strcmp(narrow, "ab") == 0); #endif long long count = -1; int smallCount = -1; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fa1136ccd..10c74b379 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -552,7 +552,7 @@ Throughout the project, every function at every stage either **does exactly what - [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); positional arguments, the `'` flag and `%m` throw when a format requires the guest formatter - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does -- [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits +- [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [socketpair](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [read, write and vector I/O](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor From 2a7b38ab75a519e128eb5e13a19735753e02bee4 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:10:21 +0200 Subject: [PATCH 168/318] fix(libSceAgc): write the KytyPS5 workload packets and return null for unregistered streams sceAgcDcbSetWorkloadsActive writes an 18-dword IT_NOP packet (stream id, 64-bit workload mask) and sceAgcDcbSetWorkloadComplete a 12-dword one (stream id, workload id, 64-bit clear mask), following KytyPS5. Both return null for a null buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. sceAgcDriverRegisterWorkloadStream and sceAgcDriverUnregisterWorkloadStream now record the stream ids the DCB calls check. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgc/CMakeLists.txt | 2 +- .../prx/libSceAgc/DcbState/src/Workload.cpp | 45 ++++++++++++++----- core/libs/prx/libSceAgc/tests/Commands.cpp | 34 +++++++++++--- .../Resource/include/Registration.hpp | 4 ++ .../Resource/src/Registration.cpp | 23 ++++++++-- docs/dev/TechnicalDebt.md | 2 +- 6 files changed, 87 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 3d44c03db..130df0566 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -68,7 +68,7 @@ endforeach() add_test_executable(agc_command_tests EXCLUDE_FROM_ALL tests/Commands.cpp) target_include_directories(agc_command_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_command_tests PRIVATE libSceAgc libc) +target_link_libraries(agc_command_tests PRIVATE libSceAgc libSceAgcDriver libc) configure_windows_unwind(agc_command_tests) if(BUILD_TESTING) set_target_properties(agc_command_tests PROPERTIES EXCLUDE_FROM_ALL OFF) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index c2db4e988..5db03f98a 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -1,20 +1,25 @@ #include "prx/libSceAgc/DcbState/include/Workload.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" +#include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" namespace { constexpr std::uint32_t OpcodeNop = 0x10; +constexpr std::uint32_t ActivePacketWords = 18; +constexpr std::uint32_t CompletePacketWords = 12; +constexpr std::uint32_t MaxWorkloadId = 63; +constexpr std::uint32_t MaxActiveCount = 63; -uint32_t* EmitNop(CommandBuffer* buf, const char* function) { - Agc::Command::Require(buf != nullptr, function, "null command buffer"); - auto* packet = Agc::Command::Allocate(buf, 2, function); - packet[0] = Agc::Command::Header(OpcodeNop, 2, 0); - packet[1] = 0; +uint32_t* NewPacket(CommandBuffer* buf, std::uint32_t words, const char* function) { + auto* packet = Agc::Command::Allocate(buf, words, function); + std::memset(packet, 0, words * sizeof(std::uint32_t)); + packet[0] = Agc::Command::Header(OpcodeNop, words, 0); return packet; } @@ -23,16 +28,32 @@ uint32_t* EmitNop(CommandBuffer* buf, const char* function) { extern "C" { uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { - (void)stream_id; - (void)workload_id; - return EmitNop(buf, __func__); + if (buf == nullptr || workload_id > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; + const std::uint64_t clearMask = ~(std::uint64_t{1} << workload_id); + auto* packet = NewPacket(buf, CompletePacketWords, __func__); + packet[1] = stream_id; + packet[2] = workload_id; + packet[3] = static_cast(clearMask); + packet[4] = static_cast(clearMask >> 32); + return packet; } uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - (void)stream_id; - (void)workload_ids; - (void)workload_count; - return EmitNop(buf, __func__); + if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount) return nullptr; + std::uint64_t mask = 0; + for (std::uint32_t index = 0; index < workload_count; ++index) { + const auto id = workload_ids[index]; + if (id > MaxWorkloadId) return nullptr; + const std::uint64_t bit = std::uint64_t{1} << id; + if ((mask & bit) != 0) return nullptr; + mask |= bit; + } + if (!AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; + auto* packet = NewPacket(buf, ActivePacketWords, __func__); + packet[1] = stream_id; + packet[2] = static_cast(mask); + packet[3] = static_cast(mask >> 32); + return packet; } } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index edb09cbac..14b049189 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -30,6 +30,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* extern "C" std::uint32_t* APS5_VABI sceAgcDcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcDcbPopMarker(CommandBuffer* buf); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t stream_id, std::uint32_t workload_id); +extern "C" int APS5_VABI sceAgcDriverRegisterWorkloadStream(std::uint32_t stream_id, const void* stream); +extern "C" int APS5_VABI sceAgcDriverUnregisterWorkloadStream(std::uint32_t stream_id); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t stream_id, const std::uint32_t* workload_ids, std::uint32_t workload_count); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); @@ -212,13 +214,33 @@ void testMarkers() { void testWorkloads() { Storage dcb; const std::uint32_t ids[2] = {3, 4}; + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2) == nullptr, "workloads active packet for an unregistered stream"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) == nullptr, "workload complete packet for an unregistered stream"); + check(dcb.buffer.cursor_up == dcb.words.data(), "unregistered stream advanced the cursor"); + const std::uint32_t record[8] = {}; + check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); const auto* active = sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2); - check(active == dcb.words.data() && active[0] == Agc::Command::Header(0x10, 2, 0) && active[1] == 0, "workloads active packet is not a plain NOP"); - const auto* complete = sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3); - check(complete == dcb.words.data() + 2 && complete[0] == Agc::Command::Header(0x10, 2, 0) && complete[1] == 0, "workload complete packet is not a plain NOP"); - check(dcb.buffer.cursor_up == dcb.words.data() + 4, "incorrect workload cursor advance"); - expectFailure([] { sceAgcDcbSetWorkloadsActive(nullptr, 1, nullptr, 0); }); - expectFailure([] { sceAgcDcbSetWorkloadComplete(nullptr, 1, 3); }); + check(active == dcb.words.data() && active[0] == Agc::Command::Header(0x10, 18, 0) && active[1] == 1 && active[2] == 0x18 && active[3] == 0, "workloads active packet"); + for (std::uint32_t word = 4; word < 18; ++word) check(active[word] == 0, "workloads active packet tail is not zero"); + const auto* complete = sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 33); + check(complete == dcb.words.data() + 18 && complete[0] == Agc::Command::Header(0x10, 12, 0) && complete[1] == 1 && complete[2] == 33 && complete[3] == 0xffffffffu && complete[4] == 0xfffffffdu, "workload complete packet"); + for (std::uint32_t word = 5; word < 12; ++word) check(complete[word] == 0, "workload complete packet tail is not zero"); + check(dcb.buffer.cursor_up == dcb.words.data() + 30, "incorrect workload cursor advance"); + const std::uint32_t duplicate[2] = {5, 5}; + const std::uint32_t outOfRange[1] = {64}; + const std::uint32_t top[1] = {63}; + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, duplicate, 2) == nullptr, "duplicate workload ids accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, outOfRange, 1) == nullptr, "workload id 64 accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, top, 1) != nullptr, "workload id 63 rejected"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 0) == nullptr, "workload count 0 accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, nullptr, 2) == nullptr, "null workload ids accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 0, ids, 2) == nullptr, "stream id 0 accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 32, ids, 2) == nullptr, "stream id 32 accepted"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 64) == nullptr, "workload complete id 64 accepted"); + check(sceAgcDcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr, "null command buffer accepted"); + check(sceAgcDcbSetWorkloadComplete(nullptr, 1, 3) == nullptr, "null command buffer accepted for complete"); + check(sceAgcDriverUnregisterWorkloadStream(1) == 0, "workload stream unregistration failed"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) == nullptr, "workload complete packet after unregistration"); } void testIndexBuffer() { diff --git a/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp b/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp index bcf672d32..d30f27839 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp +++ b/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp @@ -1,4 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_RESOURCE_INCLUDE_REGISTRATION_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_RESOURCE_INCLUDE_REGISTRATION_HPP +#include + +extern "C" bool AgcDriverWorkloadStreamRegistered_nid_postfix(std::uint32_t streamId); + #endif diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index 9fe7f3639..fe08199be 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -2,15 +2,27 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" // Resource registration feeds GPU debugging tools, none of which are attached; the game treats this code as benign. static constexpr int SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE = static_cast(0x8A6C9018); static constexpr uint32_t RESOURCE_REGISTRATION_MAX_NAME_LENGTH = 0xfc; +static constexpr uint32_t WORKLOAD_STREAM_MIN_ID = 1; +static constexpr uint32_t WORKLOAD_STREAM_MAX_ID = 31; + +static std::mutex g_workloadStreamLock; +static uint32_t g_workloadStreams = 0; extern "C" { +bool AgcDriverWorkloadStreamRegistered_nid_postfix(uint32_t streamId) { + if (streamId < WORKLOAD_STREAM_MIN_ID || streamId > WORKLOAD_STREAM_MAX_ID) return false; + std::lock_guard lock(g_workloadStreamLock); + return (g_workloadStreams & (1u << streamId)) != 0; +} + int APS5_VABI sceAgcDriverRegisterOwner(uint32_t* owner_handle, const char* name) { (void)owner_handle; (void)name; @@ -37,8 +49,10 @@ int APS5_VABI sceAgcDriverRegisterMultipleResources(uint32_t* resourceHandle, ui } int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) { - (void)stream_id; - (void)stream; + if (stream != nullptr && stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { + std::lock_guard lock(g_workloadStreamLock); + g_workloadStreams |= 1u << stream_id; + } return 0; } @@ -69,7 +83,10 @@ int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner(uint32_t owner_handle) } int APS5_VABI sceAgcDriverUnregisterWorkloadStream(uint32_t stream_id) { - (void)stream_id; + if (stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { + std::lock_guard lock(g_workloadStreamLock); + g_workloadStreams &= ~(1u << stream_id); + } return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fd97d1e42..ed84c6c51 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -659,7 +659,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceQoSEnd](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; no endpoint is implemented - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - emit a plain two-dword NOP packet and keep no workload state: the stream and workload ids are not recorded, so a capture tool never sees them. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. The packet the console writes is unknown. +- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws - [pthread_cond_reltimedwait_np](../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console From 5864ee583931a0c827e46b1173fe5a04fdb836dc Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 12:38:06 +1300 Subject: [PATCH 169/318] fix(agc): report arena host holes as stored over on Windows StoredOver still read block stamps for a range in a host region inside the guest arena interval, which Watched reports as unwatched since #3594. --- core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index b764c499d..de2fb32eb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1307,6 +1307,9 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati const auto lock = lockTracker(tracker); tracker.initialize(); if (!tracker.watched || generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return true; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return true; +#endif const auto end = address + bytes; const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(end - 1); From a4502cd4bd3f852b2ee843e3939e479420740810 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:07:20 +0200 Subject: [PATCH 170/318] feat(libkernel): implement getlogin_r using the host login name --- core/libs/prx/libkernel/CMakeLists.txt | 14 ++- core/libs/prx/libkernel/System/src/Login.cpp | 56 +++++++++++ core/libs/tests/GuestGetlogin.cpp | 98 ++++++++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libkernel/System/src/Login.cpp create mode 100644 core/libs/tests/GuestGetlogin.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a85c9ecaa..95dc41cd0 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -54,6 +54,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Sandbox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Login.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SoftwareVersion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Signals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp @@ -70,7 +71,7 @@ target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) target_link_libraries(libkernel PRIVATE ${CMAKE_DL_LIBS}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) - target_link_libraries(libkernel PRIVATE ws2_32 shell32) + target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32) target_compile_definitions(libkernel PRIVATE _WIN32_WINNT=0x0602) endif() configure_windows_unwind(libkernel) @@ -712,6 +713,17 @@ if(BUILD_TESTING) set_tests_properties(guest_getrusage PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_getlogin_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetlogin.cpp) + target_include_directories(guest_getlogin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_getlogin_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_getlogin_tests) + add_test(NAME guest_getlogin COMMAND guest_getlogin_tests) + set_tests_properties(guest_getlogin PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_getlogin_tests PRIVATE advapi32) + set_tests_properties(guest_getlogin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sched_priority_tests ${LIBS_INCLUDE_DIR}/tests/GuestSchedPriority.cpp) target_include_directories(guest_sched_priority_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sched_priority_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Login.cpp b/core/libs/prx/libkernel/System/src/Login.cpp new file mode 100644 index 000000000..12fde8723 --- /dev/null +++ b/core/libs/prx/libkernel/System/src/Login.cpp @@ -0,0 +1,56 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace { +int ReadLogin(char* output, int size) { +#ifdef _WIN32 + std::array name{}; + DWORD count = static_cast(name.size()); + if (!GetUserNameW(name.data(), &count)) + throw std::system_error(GetLastError(), std::system_category(), "getlogin_r: GetUserNameW"); + std::array utf8{}; + const int length = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, name.data(), + static_cast(count), utf8.data(), static_cast(utf8.size()), nullptr, nullptr); + if (length == 0) + throw std::system_error(GetLastError(), std::system_category(), "getlogin_r: UTF-8 conversion"); + if (length > size) return 34; + std::memcpy(output, utf8.data(), static_cast(length)); + return 0; +#else + const int error = ::getlogin_r(output, static_cast(size)); + switch (error) { + case 0: return 0; + case ENOENT: return 2; + case EINTR: return 4; + case EIO: return 5; + case ENXIO: return 6; + case ENOMEM: return 12; + case EACCES: return 13; + case ENFILE: return 23; + case EMFILE: return 24; + case ENOTTY: return 25; + case ERANGE: return 34; + case ENOSYS: return 78; + default: throw std::system_error(error, std::generic_category(), "getlogin_r: unsupported host error"); + } +#endif +} +} + +extern "C" int APS5_VABI getlogin_r_nid_postfix(char* output, int size) { + if (size <= 0) return 34; + if (output == nullptr) return 14; + const int saved = errno; + const int result = ReadLogin(output, size); + errno = saved; + return result; +} diff --git a/core/libs/tests/GuestGetlogin.cpp b/core/libs/tests/GuestGetlogin.cpp new file mode 100644 index 000000000..f6326d16c --- /dev/null +++ b/core/libs/tests/GuestGetlogin.cpp @@ -0,0 +1,98 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +extern "C" int APS5_VABI getlogin_r_nid_postfix(char*, int); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "getlogin_r check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static int HostLogin(std::array& output) { +#ifdef _WIN32 + wchar_t name[UNLEN + 1]; + DWORD count = UNLEN + 1; + Require(GetUserNameW(name, &count) != 0); + Require(WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, name, -1, + output.data(), static_cast(output.size()), nullptr, nullptr) > 0); + return 0; +#else + const int error = ::getlogin_r(output.data(), output.size()); + switch (error) { + case 0: return 0; + case ENOENT: return 2; + case EINTR: return 4; + case EIO: return 5; + case ENXIO: return 6; + case ENOMEM: return 12; + case EACCES: return 13; + case ENFILE: return 23; + case EMFILE: return 24; + case ENOTTY: return 25; + case ERANGE: return 34; + case ENOSYS: return 78; + default: std::abort(); + } +#endif +} + +static void RequireLogin(const std::string& expected) { + std::vector output(expected.size() + 3, '\x5A'); + *__error_nid_postfix() = 12345; + Require(getlogin_r_nid_postfix(output.data() + 1, static_cast(expected.size() + 1)) == 0); + Require(*__error_nid_postfix() == 12345); + Require(output.front() == '\x5A' && output.back() == '\x5A'); + Require(std::memcmp(output.data() + 1, expected.c_str(), expected.size() + 1) == 0); + output.assign(output.size(), '\x5A'); + const auto saved = output; + Require(getlogin_r_nid_postfix(output.data() + 1, static_cast(expected.size())) == 34); + Require(*__error_nid_postfix() == 12345 && output == saved); +} + +int main() { + std::array output; + output.fill('\x5A'); + const auto untouched = output; + for (int size : {0, -1, std::numeric_limits::min()}) { + *__error_nid_postfix() = 12345; + Require(getlogin_r_nid_postfix(output.data(), size) == 34); + Require(*__error_nid_postfix() == 12345 && output == untouched); + } + Require(getlogin_r_nid_postfix(nullptr, 1) == 14); + Require(*__error_nid_postfix() == 12345); + std::array host{}; + const int status = HostLogin(host); + *__error_nid_postfix() = 12345; + Require(getlogin_r_nid_postfix(output.data(), static_cast(output.size())) == status); + Require(*__error_nid_postfix() == 12345); + if (status != 0) { + std::fprintf(stderr, "Host has no login name: guest error %d\n", status); + return 0; + } + Require(std::strcmp(output.data(), host.data()) == 0); + const std::string expected(host.data()); + RequireLogin(expected); + std::array readers; + for (auto& reader : readers) reader = std::thread([&] { + for (int i = 0; i != 20; ++i) RequireLogin(expected); + }); + for (auto& reader : readers) reader.join(); +} From f88be06af2c2f6a6e14dd9d1137b51525953d66d Mon Sep 17 00:00:00 2001 From: lorkydey Date: Mon, 12 Oct 2026 01:23:26 +0200 Subject: [PATCH 171/318] fix(libkernel): resolve getlogin_r from the effective host account --- core/libs/prx/libkernel/CMakeLists.txt | 8 ++++ core/libs/prx/libkernel/System/src/Login.cpp | 29 +++++++-------- core/libs/tests/GuestGetlogin.cpp | 37 +++++-------------- core/libs/tests/GuestGetloginNoSession.cpp | 39 ++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 + 5 files changed, 72 insertions(+), 43 deletions(-) create mode 100644 core/libs/tests/GuestGetloginNoSession.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 95dc41cd0..c4a20907a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -719,6 +719,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_getlogin_tests) add_test(NAME guest_getlogin COMMAND guest_getlogin_tests) set_tests_properties(guest_getlogin PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_getlogin_no_session_tests + ${LIBS_INCLUDE_DIR}/tests/GuestGetloginNoSession.cpp + System/src/Login.cpp) + target_include_directories(guest_getlogin_no_session_tests PRIVATE ${LIBS_INCLUDE_DIR}) + add_test(NAME guest_getlogin_no_session COMMAND guest_getlogin_no_session_tests) + set_tests_properties(guest_getlogin_no_session PROPERTIES TIMEOUT 20) + endif() if(WIN32) target_link_libraries(guest_getlogin_tests PRIVATE advapi32) set_tests_properties(guest_getlogin PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libkernel/System/src/Login.cpp b/core/libs/prx/libkernel/System/src/Login.cpp index 12fde8723..80f9eaabc 100644 --- a/core/libs/prx/libkernel/System/src/Login.cpp +++ b/core/libs/prx/libkernel/System/src/Login.cpp @@ -7,6 +7,7 @@ #include #include #else +#include #include #endif @@ -26,22 +27,18 @@ int ReadLogin(char* output, int size) { std::memcpy(output, utf8.data(), static_cast(length)); return 0; #else - const int error = ::getlogin_r(output, static_cast(size)); - switch (error) { - case 0: return 0; - case ENOENT: return 2; - case EINTR: return 4; - case EIO: return 5; - case ENXIO: return 6; - case ENOMEM: return 12; - case EACCES: return 13; - case ENFILE: return 23; - case EMFILE: return 24; - case ENOTTY: return 25; - case ERANGE: return 34; - case ENOSYS: return 78; - default: throw std::system_error(error, std::generic_category(), "getlogin_r: unsupported host error"); - } + std::array storage{}; + passwd entry{}; + passwd* account = nullptr; + const int error = ::getpwuid_r(::geteuid(), &entry, storage.data(), storage.size(), &account); + if (error != 0) + throw std::system_error(error, std::generic_category(), "getlogin_r: getpwuid_r"); + if (account == nullptr || account->pw_name == nullptr || account->pw_name[0] == '\0') + throw std::runtime_error("getlogin_r: host effective user has no account name"); + const std::size_t length = std::strlen(account->pw_name) + 1; + if (length > static_cast(size)) return 34; + std::memcpy(output, account->pw_name, length); + return 0; #endif } } diff --git a/core/libs/tests/GuestGetlogin.cpp b/core/libs/tests/GuestGetlogin.cpp index f6326d16c..b006a283f 100644 --- a/core/libs/tests/GuestGetlogin.cpp +++ b/core/libs/tests/GuestGetlogin.cpp @@ -12,6 +12,7 @@ #include #include #else +#include #include #endif @@ -26,31 +27,19 @@ static void Check(bool condition, int line) { } #define Require(value) Check((value), __LINE__) -static int HostLogin(std::array& output) { +static std::string HostLogin() { #ifdef _WIN32 wchar_t name[UNLEN + 1]; DWORD count = UNLEN + 1; Require(GetUserNameW(name, &count) != 0); + std::array output{}; Require(WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, name, -1, output.data(), static_cast(output.size()), nullptr, nullptr) > 0); - return 0; + return output.data(); #else - const int error = ::getlogin_r(output.data(), output.size()); - switch (error) { - case 0: return 0; - case ENOENT: return 2; - case EINTR: return 4; - case EIO: return 5; - case ENXIO: return 6; - case ENOMEM: return 12; - case EACCES: return 13; - case ENFILE: return 23; - case EMFILE: return 24; - case ENOTTY: return 25; - case ERANGE: return 34; - case ENOSYS: return 78; - default: std::abort(); - } + const passwd* account = ::getpwuid(::geteuid()); + Require(account != nullptr && account->pw_name != nullptr && account->pw_name[0] != '\0'); + return account->pw_name; #endif } @@ -78,17 +67,11 @@ int main() { } Require(getlogin_r_nid_postfix(nullptr, 1) == 14); Require(*__error_nid_postfix() == 12345); - std::array host{}; - const int status = HostLogin(host); + const std::string expected = HostLogin(); *__error_nid_postfix() = 12345; - Require(getlogin_r_nid_postfix(output.data(), static_cast(output.size())) == status); + Require(getlogin_r_nid_postfix(output.data(), static_cast(output.size())) == 0); Require(*__error_nid_postfix() == 12345); - if (status != 0) { - std::fprintf(stderr, "Host has no login name: guest error %d\n", status); - return 0; - } - Require(std::strcmp(output.data(), host.data()) == 0); - const std::string expected(host.data()); + Require(std::strcmp(output.data(), expected.c_str()) == 0); RequireLogin(expected); std::array readers; for (auto& reader : readers) reader = std::thread([&] { diff --git a/core/libs/tests/GuestGetloginNoSession.cpp b/core/libs/tests/GuestGetloginNoSession.cpp new file mode 100644 index 000000000..c9f29f551 --- /dev/null +++ b/core/libs/tests/GuestGetloginNoSession.cpp @@ -0,0 +1,39 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI getlogin_r_nid_postfix(char*, int); + +static int loginError = ENXIO; + +extern "C" int getlogin_r(char* output, std::size_t size) { + if (loginError != 0) return loginError; + constexpr char sessionName[] = "anyps5-other-login-session"; + if (size < sizeof(sessionName)) return ERANGE; + std::memcpy(output, sessionName, sizeof(sessionName)); + return 0; +} + +int main() { + const passwd* account = ::getpwuid(::geteuid()); + if (account == nullptr || account->pw_name == nullptr || account->pw_name[0] == '\0') + std::abort(); + const std::string expected(account->pw_name); + for (int error : {ENXIO, ENOENT, 0}) { + loginError = error; + std::array output{}; + errno = 12345; + const int result = getlogin_r_nid_postfix(output.data(), static_cast(output.size())); + if (result != 0 || errno != 12345 || output.data() != expected) { + std::fprintf(stderr, "getlogin_r changed with host login-session result %d: guest result %d\n", + error, result); + return 1; + } + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..b208b0061 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -50,6 +50,8 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info +- [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`. + - [sceHttpUriBuild](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. - [sceCoredumpGetThreadContextInfo](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it - [sceNpWebApi2PushEventAbortHandle](../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. From 5b1868d23d9d931e250dff3d01d10809fdbf4dbd Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 12:50:24 +1300 Subject: [PATCH 172/318] fix(libkernel): reject mprotect of host memory that is not a guest thread stack The Windows path from #3788 changed the protection of any committed private host region outside the guest registry, including a range crossing a guest stack's lower bound. Ranges inside a live guest thread stack keep the path from #2982. --- .../libkernel/DirectMemory/DirectMemory.cpp | 4 --- core/libs/tests/GuestKernelStackProtect.cpp | 31 +++++++++++++++++++ core/libs/tests/GuestMemory.cpp | 19 ------------ 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 8d1916f7b..9eeb24861 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -921,10 +921,6 @@ int DoMprotect(const void* addr, size_t len, int prot) { else if (IsGuestModuleImage(memory.AllocationBase)) mutation.RegisterImage(memory.AllocationBase); else throw std::invalid_argument("Memory protection of a foreign image is not supported"); } - if (memory.Type == MEM_PRIVATE && memory.State == MEM_COMMIT && memory.RegionSize >= bytes && !mutation.Overlaps(pointer, bytes)) { - if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); - return 0; - } #else mutation.RegisterMainImage(); #endif diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp index 861844474..66aed4047 100644 --- a/core/libs/tests/GuestKernelStackProtect.cpp +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -9,6 +9,7 @@ #include #else #include +#include #endif extern "C" { @@ -21,6 +22,7 @@ int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protec static constexpr int SCE_OK = 0; static constexpr std::uintptr_t Page = 0x4000; static constexpr std::size_t GuardBytes = 0x10000; +static constexpr std::size_t HostBytes = 0x40000; static void Require(bool value) { if (!value) std::abort(); } @@ -54,6 +56,23 @@ static PageState QueryPage(std::uintptr_t address) { #endif } +static void* MapHostPages() { +#ifdef _WIN32 + return VirtualAlloc(nullptr, HostBytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); +#else + void* mapped = mmap(nullptr, HostBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return mapped == MAP_FAILED ? nullptr : mapped; +#endif +} + +static void UnmapHostPages(void* pages) { +#ifdef _WIN32 + Require(VirtualFree(pages, 0, MEM_RELEASE) != 0); +#else + Require(munmap(pages, HostBytes) == 0); +#endif +} + static void* APS5_VABI Worker(void* arg) { auto& passed = *static_cast*>(arg); int local = 0; @@ -82,6 +101,18 @@ static void* APS5_VABI Worker(void* arg) { static_cast(start)[0] = 3; const auto outside = reinterpret_cast(start) & ~(Page - 1); if (outside >= Page) Require(mprotect_nid_postfix(reinterpret_cast(outside - Page), 2 * Page, 3) == -1); + if (outside >= 2 * Page) Require(mprotect_nid_postfix(reinterpret_cast(outside - 2 * Page), Page, 3) == -1); + const auto stackBytes = reinterpret_cast(end) - reinterpret_cast(start); + Require(mprotect_nid_postfix(start, stackBytes, 0xc2) == 0); + Require(QueryPage(low).writable); + static_cast(start)[0] = 4; + Require(mprotect_nid_postfix(start, stackBytes, 3) == 0); + void* host = MapHostPages(); + Require(host != nullptr); + Require(mprotect_nid_postfix(host, HostBytes, 1) == -1); + Require(QueryPage(reinterpret_cast(host)).writable); + static_cast(host)[HostBytes - 1] = 5; + UnmapHostPages(host); passed.store(true); return nullptr; } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 427a6d064..74beb7554 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1698,31 +1698,12 @@ static void CheckAvailableDirectMemoryIsLargestFreeBlock() { Require(sceKernelAvailableDirectMemorySize(top, top + window, 0, &start, &size) == 0); Require(start == top && size == static_cast(window)); } -#ifdef _WIN32 -static void CheckMprotectOfHostMemoryOutsideTheGuestRegistry() { - constexpr std::size_t length = 0x40000; - void* host = VirtualAlloc(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); - Require(host != nullptr); - Require(sceKernelMprotect(host, length, 0x3) == 0); - static_cast(host)[length - 1] = 7; - Require(static_cast(host)[length - 1] == 7); - Require(sceKernelMprotect(host, length, 0x1) == 0); - MEMORY_BASIC_INFORMATION info{}; - Require(VirtualQuery(host, &info, sizeof(info)) == sizeof(info) && info.Protect == PAGE_READONLY); - Require(sceKernelMprotect(host, length, 0x3) == 0); - Require(VirtualQuery(host, &info, sizeof(info)) == sizeof(info) && info.Protect == PAGE_READWRITE); - Require(VirtualFree(host, 0, MEM_RELEASE) != 0); -} -#endif int main() { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "1000"); #endif CheckAvailableDirectMemoryIsLargestFreeBlock(); -#ifdef _WIN32 - CheckMprotectOfHostMemoryOutsideTheGuestRegistry(); -#endif CheckVirtualQuerySplitFlexibleRanges(); CheckVirtualQueryForNonReadableGuestRange(); CheckReleaseFlexibleMemory(); From fded8a7e7fa8b5c6c18ecbd8eef23bef5b620ad2 Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd Date: Sun, 11 Oct 2026 20:09:01 +0400 Subject: [PATCH 173/318] fix(shader): prove masked writes through guarded first-lane reads --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 128 +++++++++++++++++- .../src/MaskedSelectEliminator.cpp | 20 ++- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 02a244738..030146961 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -209,7 +209,7 @@ int main() { passed &= run("cross-lane reads must keep the select", [] { bool ok = true; - for (const IrOpcode opcode : {IrOpcode::ReadFirstLane, IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { + for (const IrOpcode opcode : {IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { Builder b; auto& exec = b.mask(16u); auto& old = b.lane(); @@ -227,6 +227,132 @@ int main() { return ok && b.eliminate() == 0u && !removed(written); }); + passed &= run("a first-lane read guarded at both ends loses the source select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& narrow = b.logicalAnd(exec, b.mask(8u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &narrow}); + b.keep(b.select(narrow, read, old)); + return b.eliminate() == 1u && removed(written); + }); + + passed &= run("an unguarded first-lane result keeps the source select for empty exec", [] { + Builder b; + auto& exec = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&b.lane(), &b.constant(0u)}); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + b.keep(b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &empty})); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a first-lane result read under the write mask keeps the select when read exec is narrower", [] { + Builder b; + auto& exec = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&b.lane(), &b.constant(0u)}); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &empty}); + b.keep(b.select(exec, read, old)); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a first-lane read under a wider exec keeps the select", [] { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &wide}); + b.keep(b.select(wide, read, old)); + return b.eliminate() == 0u && !removed(written); + }); + + const auto selectedExec = [](bool negate, bool widerArm) { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + auto& selector = negate ? b.emit(IrOpcode::LogicalNot, IrType::Bool, {&exec}) : exec; + auto& later = b.emit(IrOpcode::SelectU1, IrType::Bool, {&selector, negate ? (widerArm ? &wide : &empty) : &wide, negate ? &wide : (widerArm ? &wide : &empty)}); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("selected boolean exec implies the write mask through its selector", [&] { + for (bool negate : {false, true}) { + const auto [count, gone] = selectedExec(negate, false); + if (count != 1u || !gone) return false; + } + return true; + }); + + passed &= run("selected boolean exec with a wider unguarded arm keeps the select", [&] { + for (bool negate : {false, true}) { + const auto [count, gone] = selectedExec(negate, true); + if (count != 0u || gone) return false; + } + return true; + }); + + const auto rebuiltExec = [](bool mismatchedHigh) { + Builder b; + const auto [low, high] = b.ballotWords(b.mask(16u)); + const auto [wideLow, wideHigh] = b.ballotWords(b.mask(48u)); + auto& exec = b.threadBit(*low, *high); + auto& later = b.threadBit(*low, mismatchedHigh ? *wideHigh : *high); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec rebuilt from the same words implies the write mask", [&] { + const auto [count, gone] = rebuiltExec(false); + return count >= 1u && gone; + }); + + passed &= run("matching low mask words alone do not imply a wave64 write mask", [&] { + const auto [count, gone] = rebuiltExec(true); + return count == 0u && !gone; + }); + + const auto firstLaneInLoop = [](bool carriedResult) { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrBlock& entry = *b.block; + IrBlock& loop = b.newBlock(); + entry.AddBranch(&loop); + loop.AddBranch(&loop); + b.block = &loop; + auto& loopExec = b.logicalAnd(exec, b.mask(8u)); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &loopExec}); + if (carriedResult) { + auto& carried = b.phi(IrType::U32, entry, old); + carried.AddPhiOperand(&loop, &read); + b.keep(b.select(loopExec, carried, old)); + } else { + b.keep(b.select(loopExec, read, old)); + } + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a loop-local first-lane result guarded in its own iteration loses the source select", [&] { + const auto [count, gone] = firstLaneInLoop(false); + return count == 1u && gone; + }); + + passed &= run("a first-lane result carried to another loop iteration keeps the source select", [&] { + const auto [count, gone] = firstLaneInLoop(true); + return count == 0u && !gone; + }); + passed &= run("a branch condition derived from the write keeps the select", [] { Builder b; auto& exec = b.mask(16u); diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 51ec69506..0ab716a84 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -382,6 +382,11 @@ public: result = true; } else if (condition->Opcode() == IrOpcode::LogicalAnd) { result = Implies(condition->Argument(0), depth + 1u) || Implies(condition->Argument(1), depth + 1u); + } else if (condition->Opcode() == IrOpcode::SelectU1) { + const IrValue* selector = condition->Argument(0)->Resolve(); + const bool trueCase = Implies(condition->Argument(1), depth + 1u) || Implies(selector, depth + 1u); + const bool falseCase = Implies(condition->Argument(2), depth + 1u) || (selector->Opcode() == IrOpcode::LogicalNot && Implies(selector->Argument(0), depth + 1u)); + result = trueCase && falseCase; } else if (condition->IsPhi()) { result = merges(*condition); for (std::size_t index = 0; result && index < condition->ArgumentCount(); ++index) { @@ -443,6 +448,8 @@ private: bool wordsImply(const IrValue* low, const IrValue* high, std::uint32_t depth) { if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; + const auto maskBit = threadBit(mask, waveSize); + if (maskBit && low == maskBit->low && (waveSize == 32u || high == maskBit->high)) return true; const ImplicationKey key{low, high}; if (const auto known = lookup(key)) return *known; if (depth > ImplicationDepthLimit || work == 0) return false; @@ -537,10 +544,11 @@ std::size_t walkLimit(const IrProgram& program) { class MaskedLaneWalk { public: - MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), uniform(uniformEntryBlocks(program)), limit(walkLimit(program)), implications(regions, uniform, program.WaveSize(), work) {} + MaskedLaneWalk(const IrProgram& program, const std::vector& regions, std::uint32_t nesting = 0) : program(program), regions(regions), uniform(uniformEntryBlocks(program)), limit(walkLimit(program)), implications(regions, uniform, program.WaveSize(), work), nesting(nesting) {} bool UnobservedWhereMasked(const IrValue& select, const IrValue* mask) { const bool acrossLoops = mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once; + const bool sameIteration = !acrossLoops && select.Opcode() == IrOpcode::ReadFirstLane; work = limit; implications.Reset(mask, acrossLoops); const bool quadMasked = program.Resources().stage == IrShaderStage::Pixel && wholeQuadSource(mask, program.WaveSize()) != nullptr; @@ -553,10 +561,17 @@ public: pending.pop_back(); for (const IrUse& use : value->OperandUses()) { const IrValue* user = use.user; - if (!followable(regions, user->Parent(), acrossLoops)) return false; + if (sameIteration ? user->Parent() != select.Parent() || user->IsPhi() : !followable(regions, user->Parent(), acrossLoops)) return false; if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; + if (user->Opcode() == IrOpcode::ReadFirstLane && use.operand == 0u && nesting < DepthLimit && implications.Implies(user->Argument(1))) { + MaskedLaneWalk nested(program, regions, nesting + 1u); + nested.limit = work; + const bool unobserved = nested.UnobservedWhereMasked(*user, user->Argument(1)->Resolve()); + work = nested.work; + if (unobserved) continue; + } if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user) && !(quadMasked && user->Opcode() == IrOpcode::ImageSampleRaw)) return false; if (firstVisit(*user)) { if (work == 0) return false; @@ -592,6 +607,7 @@ private: std::vector pending; std::vector visits; std::uint32_t epoch = 0; + std::uint32_t nesting = 0; }; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..e8f865468 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -374,6 +374,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. - [sem_timedwait](../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. +- [MaskedSelectEliminator](../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; proofs stop at the existing work and nesting limits. - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. From 2446a2648a7a743c2b7890a25414976d39d41213 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 20:59:17 -0300 Subject: [PATCH 174/318] fix(libkernel): return the console's errors from the scePthreadAttr calls On a PS5 (firmware 13.60) the scePthreadAttr setters and getters return EINVAL for a null or uninitialized attribute and for a null output, ENOTSUP for a policy other than SCHED_FIFO and SCHED_RR, an inheritance other than explicit and inherit, and a priority outside 256 to 767, and EINVAL for an invalid detach state. Here they threw, or stored any policy, inheritance and priority. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ core/libs/prx/libkernel/Pthread/src/Attr.cpp | 39 +++++---- core/libs/tests/GuestPthreadAttrErrors.cpp | 85 ++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 118 insertions(+), 17 deletions(-) create mode 100644 core/libs/tests/GuestPthreadAttrErrors.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a85c9ecaa..2191b3c22 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -365,6 +365,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_attr_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAttrErrors.cpp) + target_include_directories(guest_pthread_attr_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_attr_errors_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_attr_errors_tests) + add_test(NAME guest_pthread_attr_errors COMMAND guest_pthread_attr_errors_tests) + set_tests_properties(guest_pthread_attr_errors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_attr_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadStack.cpp) target_include_directories(guest_pthread_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_stack_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index ff5cba730..fcead2c93 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -33,29 +33,31 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr) { } int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; delete *attr; *attr = nullptr; return SCE_OK; } int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int detachstate) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (detachstate != DETACH_JOINABLE && detachstate != DETACH_DETACHED) - throw std::runtime_error(std::string(__func__) + ": invalid state"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (detachstate != DETACH_JOINABLE && detachstate != DETACH_DETACHED) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_detachstate = detachstate; return SCE_OK; } int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param) { - if (!attr || !*attr || !param) - throw std::runtime_error(std::string(__func__) + ": null arg"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int HighestPriority = 256; + constexpr int LowestPriority = 767; + if (!param || param->sched_priority < HighestPriority || param->sched_priority > LowestPriority) + return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_schedpriority = param->sched_priority; return SCE_OK; } int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksize) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; if (stacksize < MIN_STACK_SIZE) return SCE_KERNEL_ERROR_EINVAL; #ifdef _WIN32 if (stacksize > std::numeric_limits::max() - 0xffffu) @@ -88,14 +90,14 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { } int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask) { - if (!attr || !*attr || !mask) throw std::runtime_error("scePthreadAttrGetaffinity: null argument"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (!mask) throw std::runtime_error("scePthreadAttrGetaffinity: null argument"); *mask = (*attr)->_affinity; return SCE_OK; } int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!state) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !state) return SCE_KERNEL_ERROR_EINVAL; *state = (*attr)->_detachstate; return SCE_OK; } @@ -108,8 +110,7 @@ int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, size_t* guard_ } int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!param) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !param) return SCE_KERNEL_ERROR_EINVAL; param->sched_priority = (*attr)->_schedpriority; return SCE_OK; } @@ -129,14 +130,13 @@ int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_a } int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!stack_size) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !stack_size) return SCE_KERNEL_ERROR_EINVAL; *stack_size = (*attr)->_stacksize; return SCE_OK; } int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask) { - if (!attr || !*attr) throw std::runtime_error("scePthreadAttrSetaffinity: null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_affinity = mask; (*attr)->_affinityExplicit = true; return SCE_OK; @@ -149,13 +149,18 @@ int APS5_VABI scePthreadAttrSetguardsize(PthreadAttr* attr, size_t guard_size) { } int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inherit_sched) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int ExplicitSched = 0; + constexpr int InheritSched = 4; + if (inherit_sched != ExplicitSched && inherit_sched != InheritSched) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_inheritsched = inherit_sched; return SCE_OK; } int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int SchedRr = 3; + if (policy != SCHED_FIFO_PS5 && policy != SchedRr) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_schedpolicy = policy; return SCE_OK; } diff --git a/core/libs/tests/GuestPthreadAttrErrors.cpp b/core/libs/tests/GuestPthreadAttrErrors.cpp new file mode 100644 index 000000000..259ee7789 --- /dev/null +++ b/core/libs/tests/GuestPthreadAttrErrors.cpp @@ -0,0 +1,85 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* stackSize); +int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int state); +int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state); +int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param); +int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); +int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_EOPNOTSUPP = static_cast(0x8002002Du); + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + + for (std::size_t size : {std::size_t{0}, std::size_t{1}, std::size_t{16383}}) + Require(scePthreadAttrSetstacksize(&attr, size) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetstacksize(&attr, 16384) == SCE_OK); + + for (int state : {-1, 2, 3, 100}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_KERNEL_ERROR_EINVAL); + for (int state : {0, 1}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_OK); + + for (int policy : {-1, 0, 2, 4}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int policy : {1, 3}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + + for (int inherit : {-1, 1, 2, 8}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int inherit : {0, 4}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_OK); + + KernelSchedParam param{}; + for (int priority : {-1, 0, 255, 768, 900, 960}) { + param.sched_priority = priority; + Require(scePthreadAttrSetschedparam(&attr, ¶m) == SCE_KERNEL_ERROR_EOPNOTSUPP); + } + Require(scePthreadAttrSetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int priority : {256, 767}) { + param.sched_priority = priority; + Require(scePthreadAttrSetschedparam(&attr, ¶m) == SCE_OK); + KernelSchedParam read{}; + Require(scePthreadAttrGetschedparam(&attr, &read) == SCE_OK && read.sched_priority == priority); + } + for (KernelCpumask mask : {KernelCpumask{0}, KernelCpumask{1}, ~KernelCpumask{0}}) + Require(scePthreadAttrSetaffinity(&attr, mask) == SCE_OK); + + std::size_t size = 0; + int state = 0; + KernelCpumask mask = 0; + Require(scePthreadAttrGetstacksize(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetdetachstate(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + + for (PthreadAttr* missing : {static_cast(nullptr), static_cast(&attr)}) { + PthreadAttr unset = nullptr; + PthreadAttr* target = missing ? &unset : nullptr; + Require(scePthreadAttrSetstacksize(target, 16384) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetdetachstate(target, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetschedpolicy(target, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetinheritsched(target, 0) == SCE_KERNEL_ERROR_EINVAL); + param.sched_priority = 700; + Require(scePthreadAttrSetschedparam(target, ¶m) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetaffinity(target, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetstacksize(target, &size) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetdetachstate(target, &state) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedparam(target, ¶m) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetaffinity(target, &mask) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrDestroy(target) == SCE_KERNEL_ERROR_EINVAL); + } + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..bb7ea1efa 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -191,6 +191,7 @@ Throughout the project, every function at every stage either **does exactly what - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a rwlock held for reading or writing returns `EBUSY` and keeps its handle, as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does; FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_rwlock.c) and shadPS4 free it without checking, and the console's answer is unverified. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written +- [scePthreadAttr setters and getters](../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs - [sceKernelGetAvailableCpumask](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. Any value is accepted and threads created from the attribute are not scheduled differently; what solo scheduling does on the console and which values it rejects are unverified. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it From fc7a9650f98ffe6b496408b3664b0f54a4110e30 Mon Sep 17 00:00:00 2001 From: Avijit Singh Date: Mon, 12 Oct 2026 05:32:09 +0530 Subject: [PATCH 175/318] style(agc): remove unnecessary shutdown explanation Keep the tracker initialization fix without an explanatory code comment or technical-debt entry, as requested in review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp | 1 - docs/dev/TechnicalDebt.md | 4 ---- 2 files changed, 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 7201c4124..fa645dd13 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -55,7 +55,6 @@ void Driver::stop() { } Driver::Driver() { - // Driver teardown uses the write tracker; initialize it before this function-local static completes. static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9863562e2..0c72b62a7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -9,10 +9,6 @@ - libwinpthread-1.dll - winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) are locked once when created. The host `std::shared_mutex` objects in [Recompiler.cpp](../../core/shader/recompiler/Recompiler.cpp) and [GuestWriteWatch.cpp](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) are not, and can still race on their first contended use. -### Shutdown - -- On Windows, console-control termination can run function-local static destructors through `ExitProcess`. `Driver::~Driver()` stops GPU state, which can access the guest write tracker. `Driver::Driver()` therefore initializes the tracker before the `Driver` singleton finishes construction, so reverse static-destruction order keeps the tracker alive through driver teardown. - ### Silent stubs Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: From b9b5bcc19070fc806b253ec7845af0cca1675e09 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 21:04:26 -0300 Subject: [PATCH 176/318] fix(libkernel): return the console's errors from the SCE mutex calls On a PS5 (firmware 13.60) the SCE mutex attribute calls return EINVAL for a null or uninitialized attribute, scePthreadMutexattrSetprotocol accepts 0 to 2, scePthreadMutexInit takes an uninitialized attribute as the default one, and destroying a destroyed mutex returns EINVAL. Here these threw, including for the priority protection protocol. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 18 +++---- core/libs/tests/GuestKernelErrors.cpp | 8 +-- core/libs/tests/GuestPthreadMutexErrors.cpp | 50 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 core/libs/tests/GuestPthreadMutexErrors.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a85c9ecaa..223e18d94 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -355,6 +355,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_mutexattr PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_mutex_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadMutexErrors.cpp) + target_include_directories(guest_pthread_mutex_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_mutex_errors_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_mutex_errors_tests) + add_test(NAME guest_pthread_mutex_errors COMMAND guest_pthread_mutex_errors_tests) + set_tests_properties(guest_pthread_mutex_errors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_mutex_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_start_tests ${LIBS_INCLUDE_DIR}/tests/PthreadStart.cpp) target_include_directories(guest_pthread_start_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_start_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 605874cd0..4cf22db6e 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -111,7 +111,7 @@ int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr) { int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr) { if (!attr || !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); + return SCE_KERNEL_ERROR_EINVAL; delete *attr; *attr = nullptr; return 0; @@ -119,7 +119,7 @@ int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr) { int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { if (!attr || !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); + return SCE_KERNEL_ERROR_EINVAL; switch (type) { case 1: (*attr)->type = MutexType::ErrorCheck; break; case 2: (*attr)->type = MutexType::Recursive; break; @@ -132,21 +132,19 @@ int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol) { if (!attr || !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); + return SCE_KERNEL_ERROR_EINVAL; constexpr int PrioNone = 0; - constexpr int PrioInherit = 1; - if (protocol != PrioNone && protocol != PrioInherit) - throw std::invalid_argument("Mutex priority protection is unsupported"); + constexpr int PrioProtect = 2; + if (protocol < PrioNone || protocol > PrioProtect) + return SCE_KERNEL_ERROR_EINVAL; return 0; } int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char*) { if (!mutex) throw std::invalid_argument("Mutex pointer is null"); - if (attr && !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); auto replacement = std::make_unique(); - if (attr) + if (attr && *attr) replacement->_type = (*attr)->type; std::lock_guard lock(initializationMutex); std::atomic_ref(*mutex).store(replacement.release(), std::memory_order_release); @@ -158,7 +156,7 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { throw std::invalid_argument("Mutex pointer is null"); std::lock_guard lock(initializationMutex); if (*mutex == destroyedMutex()) - throw std::runtime_error("Mutex has already been destroyed"); + return SCE_KERNEL_ERROR_EINVAL; if (*mutex == adaptiveInitializer()) { std::atomic_ref(*mutex).store(destroyedMutex(), std::memory_order_release); return 0; diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 5c14f7fca..6a76e356e 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -90,13 +90,7 @@ int main() { Require(scePthreadMutexattrSettype(&attr, 5) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_NONE) == SCE_OK); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_INHERIT) == SCE_OK); - bool protectionRejected = false; - try { - scePthreadMutexattrSetprotocol(&attr, PRIO_PROTECT); - } catch (const std::invalid_argument&) { - protectionRejected = true; - } - Require(protectionRejected); + Require(scePthreadMutexattrSetprotocol(&attr, PRIO_PROTECT) == SCE_OK); PthreadMutex mutex = nullptr; Require(scePthreadMutexInit(&mutex, &attr, nullptr) == SCE_OK); Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); diff --git a/core/libs/tests/GuestPthreadMutexErrors.cpp b/core/libs/tests/GuestPthreadMutexErrors.cpp new file mode 100644 index 000000000..064469d42 --- /dev/null +++ b/core/libs/tests/GuestPthreadMutexErrors.cpp @@ -0,0 +1,50 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); +int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); +int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010u); + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + PthreadMutexattr attr = nullptr; + Require(scePthreadMutexattrInit(&attr) == SCE_OK); + for (int type : {-1, 0, 5}) Require(scePthreadMutexattrSettype(&attr, type) == SCE_KERNEL_ERROR_EINVAL); + for (int type : {1, 2, 3, 4}) Require(scePthreadMutexattrSettype(&attr, type) == SCE_OK); + for (int protocol : {-1, 3}) Require(scePthreadMutexattrSetprotocol(&attr, protocol) == SCE_KERNEL_ERROR_EINVAL); + for (int protocol : {0, 1, 2}) Require(scePthreadMutexattrSetprotocol(&attr, protocol) == SCE_OK); + + PthreadMutexattr unset = nullptr; + Require(scePthreadMutexattrSettype(&unset, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSetprotocol(&unset, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(&unset) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSettype(nullptr, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + + PthreadMutex mutex = nullptr; + Require(scePthreadMutexInit(&mutex, &unset, "unset") == SCE_OK); + Require(scePthreadMutexLock(&mutex) == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadMutexUnlock(&mutex) == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + Require(mutex == reinterpret_cast(std::uintptr_t{2})); + Require(scePthreadMutexDestroy(&mutex) == SCE_KERNEL_ERROR_EINVAL); + + Require(scePthreadMutexInit(&mutex, &attr, "attr") == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..4e9dc31ef 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -19,6 +19,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceLoginDialog](../../core/libs/prx/libSceLoginDialog/Export.cpp) - the login dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled - [libSceCdlgG2P](../../core/libs/prx/libSceCdlgG2P/Export.cpp) - the G2P dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. The signatures (`Open` and `GetResult` take a pointer, the others none) and the leading `int32` result are assumed from the other common dialogs, not verified for G2P; PPSA01289 imports it - [libSceVrSetupDialog](../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are assumed from the other common dialogs; PPSA12544 imports it +- [scePthreadMutexattrSetprotocol](../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. - [pthread_mutexattr_setprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutexattr.c) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority), but mutexes do not apply them: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority - [libSceHmd2](../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` fails, and every other export it has (and every [libSceVrTracker2](../../core/libs/prx/libSceVrTracker2/Export.cpp) export) throws. PPSA01317 skips its whole VR set-up when initialization fails From 364d7cf29f4c5f5875785ff82b129d87ebc5ecfe Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:02:21 +0200 Subject: [PATCH 177/318] fix(shader): match half-wave reductions over relatively addressed vector registers A program that uses v_movrels_b32 or v_movreld_b32 keeps every vector register from the lowest relatively addressed one up in a per-invocation window array, so its plain register accesses become LoadVectorWindow and StoreVectorWindow. The half-wave reduction matcher followed only SSA values, so a wave64 scan, v_permlanex16_b32 join and lane 31/63 read through that array threw that lane 63 is outside the 32-lane host subgroup. The matcher now reads a window load at a constant slot as the value of the last store to that slot earlier in its block, unless a store at a runtime slot lies in between; each invocation owns its array, so that is the value the load returns. A store in another block, or a v_movreld_b32 at a runtime M0 in between, still throws. This was the only refused lane 32-63 read left among Ghost of Yotei's wave64 graphics programs (one pixel program). --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 7 +++++ .../tests/execution/PixelWaveReduction.cpp | 15 ++++++----- .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 26 ++++++++++++++++++- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2a86162fa..21db2c34f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1341,6 +1341,13 @@ void verifyHalfWaveReduction() { divergent.insert(divergent.begin() + 3, 0x88fe1a7eu); divergent.insert(divergent.begin() + 2, {0xbe860480u, 0x7d821688u, 0xbe9a246au, 0xbf880001u}); require(reductions(pixel(divergent, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: keys masked inside a divergent branch did not read lane 31 as a subgroup UMin"); + auto windowed = umin; + windowed.insert(windowed.begin() + 23, 0x7e008514u); + require(reductions(pixel(windowed, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: a UMin scan of registers in a v_movreld_b32 window did not read lane 31 as a subgroup UMin on 32 lanes"); + require(halves(pixel(windowed, 64u, true), spv::OpGroupNonUniformUMin, 0xffffffffu), "half-wave reduction: a UMin scan of registers in a v_movreld_b32 window did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); + auto overwritten = umin; + overwritten.insert(overwritten.begin() + 17, {0x7ef80500u, 0x7e008514u}); + expectFailure([&] { static_cast(pixel(overwritten, 32u, true)); }, lane63, "half-wave reduction: a scan of window registers read lane 63 after a v_movreld_b32 at a runtime M0 that may overwrite them"); const char* arithmetic = "v_readlane_b32 of lane 31 of a wave64 half-wave reduction scan needs subgroup arithmetic"; expectFailure([&] { static_cast(pixel(umin, 32u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 32 lanes"); expectFailure([&] { static_cast(pixel(umin, 64u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 64 lanes"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp index 83f21e2b2..7c007900a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp @@ -194,13 +194,16 @@ int main() { }}; for (const auto& layout : layouts) { for (const auto& reduction : reductions) { - const auto code = PixelCode(reduction); - std::uint32_t partial = 0; - for (std::uint32_t pass = 0; pass < 4; ++pass) { - Draw(*device, code, layout); - partial += Check(reduction, target.subgroupSize); + for (const bool windowed : {false, true}) { + auto code = PixelCode(reduction); + if (windowed) code.insert(std::find(code.begin(), code.end(), 0x7e0e02f2u), 0x7e008514u); + std::uint32_t partial = 0; + for (std::uint32_t pass = 0; pass < 4; ++pass) { + Draw(*device, code, layout); + partial += Check(reduction, target.subgroupSize); + } + std::printf("%s%s, laid out for %u lanes on %u-lane subgroups: %u partial waves over 4 draws\n", reduction.name, windowed ? " in a v_movreld_b32 window" : "", layout.subgroupSize, target.subgroupSize, partial); } - std::printf("%s, laid out for %u lanes on %u-lane subgroups: %u partial waves over 4 draws\n", reduction.name, layout.subgroupSize, target.subgroupSize, partial); } } std::puts("pixel wave reduction tests passed"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index ec55881e3..217c580b0 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -1,5 +1,6 @@ #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" +#include "IntermediateRepresentation/IrBlock.hpp" #include "IntermediateRepresentation/IrValue.hpp" #include #include @@ -227,8 +228,31 @@ struct HalfWaveScan { const IrValue* source = nullptr; }; +const IrValue* StoredWindowValue(const IrValue* load) { + const auto* slot = load->Argument(0)->Resolve(); + const auto* block = load->Parent(); + if (slot == nullptr || !slot->HasImmediate() || block == nullptr) return nullptr; + const auto& instructions = block->Instructions(); + auto at = std::find(instructions.begin(), instructions.end(), load); + if (at == instructions.end()) return nullptr; + while (at != instructions.begin()) { + const auto* previous = *--at; + if (previous->HasImmediate() || previous->Opcode() != IrOpcode::StoreVectorWindow) continue; + const auto* stored = previous->Argument(0)->Resolve(); + if (stored == nullptr || !stored->HasImmediate()) return nullptr; + if (stored->ImmediateU32() == slot->ImmediateU32()) return previous->Argument(1)->Resolve(); + } + return nullptr; +} + const IrValue* Resolved(const IrValue* value) { - return value != nullptr ? value->Resolve() : nullptr; + value = value != nullptr ? value->Resolve() : nullptr; + while (value != nullptr && !value->HasImmediate() && value->Opcode() == IrOpcode::LoadVectorWindow) { + const auto* stored = StoredWindowValue(value); + if (stored == nullptr) break; + value = stored; + } + return value; } bool Is(const IrValue* value, IrOpcode opcode) { From 0b7e4ddf9fc3ac1dd7ed54d75f5fbbafb7a98e54 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sun, 11 Oct 2026 21:09:08 -0300 Subject: [PATCH 178/318] fix(libkernel): join the calling thread and create from an unset attribute as the console does On a PS5 (firmware 13.60) scePthreadJoin on the calling thread returns EDEADLK and scePthreadCreate with a zeroed attribute creates a thread with the default attributes. Here both threw. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++++ .../libs/prx/libkernel/Pthread/src/Thread.cpp | 5 +-- core/libs/tests/GuestPthreadJoinSelf.cpp | 39 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestPthreadJoinSelf.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a85c9ecaa..d778b430a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -507,6 +507,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_rwlock PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_join_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadJoinSelf.cpp) + target_include_directories(guest_pthread_join_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_join_self_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_join_self_tests) + add_test(NAME guest_pthread_join_self COMMAND guest_pthread_join_self_tests) + set_tests_properties(guest_pthread_join_self PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_join_self PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_key_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadKey.cpp) target_include_directories(guest_pthread_key_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_key_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index fada8e72a..042bbbfc4 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -533,7 +533,7 @@ extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name) { if (!thread || !entry) throw std::runtime_error("scePthreadCreate: null arg"); - if (attr && !*attr) throw std::runtime_error("scePthreadCreate: null attributes"); + if (attr && !*attr) attr = nullptr; auto p = std::make_unique(attr ? *attr : nullptr); bool detached = false; if (attr && *attr) detached = ((*attr)->_detachstate == DETACH_DETACHED); @@ -620,8 +620,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { if (!thread) throw std::runtime_error("scePthreadJoin: null thread"); if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; - if (thread == CurrentGuestState().current) - throw std::runtime_error("scePthreadJoin: cannot join current thread"); + if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; ThreadCancel::Check(); { std::unique_lock lock(thread->_join_mtx); diff --git a/core/libs/tests/GuestPthreadJoinSelf.cpp b/core/libs/tests/GuestPthreadJoinSelf.cpp new file mode 100644 index 000000000..742fa9d73 --- /dev/null +++ b/core/libs/tests/GuestPthreadJoinSelf.cpp @@ -0,0 +1,39 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(void); +int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** retval); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast(0x8002000Bu); +static constexpr int GUEST_EDEADLK = 11; + +static void Require(bool value) { if (!value) std::abort(); } + +static void* APS5_VABI JoinSelf(void*) { + void* result = reinterpret_cast(std::uintptr_t{0x55}); + Require(scePthreadJoin(scePthreadSelf(), &result) == SCE_KERNEL_ERROR_EDEADLK); + Require(pthread_join_nid_postfix(scePthreadSelf(), &result) == GUEST_EDEADLK); + Require(result == reinterpret_cast(std::uintptr_t{0x55})); + return reinterpret_cast(std::uintptr_t{0x1234}); +} + +int main() { + Pthread thread = nullptr; + void* result = nullptr; + Require(scePthreadCreate(&thread, nullptr, JoinSelf, nullptr, "join-self") == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == reinterpret_cast(std::uintptr_t{0x1234})); + + PthreadAttr unset = nullptr; + thread = nullptr; + result = nullptr; + Require(scePthreadCreate(&thread, &unset, JoinSelf, nullptr, "unset-attr") == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == reinterpret_cast(std::uintptr_t{0x1234})); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..06a1fd966 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -447,6 +447,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDebugRaiseException](../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents - [sigaltstack](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. +- [scePthreadJoin, scePthreadCreate](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`); here a joined thread is released, so a second join reads freed memory. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console - [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set - [cpuset_getaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified From c87c8c229bd21e61a68a74cdeaa22a63b347a6be Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Mon, 12 Oct 2026 00:12:50 +0000 Subject: [PATCH 179/318] test(agc): keep sampled cache fixture registrations separate --- .../tests/execution/SampledCacheBudget.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 0cf28122d..9d1978dbb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -42,6 +42,8 @@ constexpr std::uint32_t Type2D = 9; constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; constexpr std::size_t Surfaces = 12; constexpr std::size_t BlockBytes = SurfaceBytes * Surfaces; +constexpr std::size_t RegistrationGapBytes = 65536; +constexpr std::size_t AllocationBytes = BlockBytes + RegistrationGapBytes; constexpr std::uint64_t Budget = 1ull << 20u; constexpr std::uint64_t Ceiling = Budget + Budget / 4u; @@ -164,9 +166,9 @@ class Block { public: explicit Block(const Context& context) : context(context) { #ifdef _WIN32 - memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + memory = VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else - memory = std::aligned_alloc(65536, BlockBytes); + memory = std::aligned_alloc(65536, AllocationBytes); #endif Require(memory != nullptr, "cannot allocate the texture block"); std::memset(memory, 0x5a, BlockBytes); @@ -449,7 +451,9 @@ void viewEvictionTests(const Context& context) { const std::weak_ptr view = Sampled(context, imported.Surface(0)); Require(!view.expired() && TextureCacheUsage().sampledBytes == 0, "the view of a cached storage image did not enter the sampled texture cache at 0 bytes"); Sampled(context, snapshots.Surface(0)); - const auto capacity = static_cast(Budget / TextureCacheUsage().sampledBytes); + const auto snapshotBytes = TextureCacheUsage().sampledBytes; + Require(snapshotBytes != 0, "the snapshot fixture unexpectedly became a view of a cached storage image"); + const auto capacity = static_cast(Budget / snapshotBytes); for (std::size_t surface = 1; surface <= capacity; ++surface) Sampled(context, snapshots.Surface(surface)); Require(!view.expired(), "the sampled texture cache evicted a view of a cached storage image, which frees no memory"); } From 5eb22de16d8fee5b1bb2f1a3db77711707240b22 Mon Sep 17 00:00:00 2001 From: Archit Dewan <108434204+ArchitDewan@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:28:56 -0400 Subject: [PATCH 180/318] fix(relinker): reject empty positional paths --- core/relinker/cli/src/CliArgs.cpp | 2 ++ core/relinker/cli/tests/CliArgsTests.cpp | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index eef10daad..21d055d6b 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -71,6 +71,8 @@ Args ParseArgs(int argc, char* argv[]) { args.windowsGui = true; } else if (arg.rfind("--", 0) == 0 || arg == "unused-filter") { throw std::runtime_error("unknown option: " + arg); + } else if (arg.empty()) { + throw std::runtime_error("input and output paths must be nonempty"); } else if (args.inputPath.empty()) { args.inputPath = arg; } else if (args.outputPath.empty()) { diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index ec27e771a..3cc339e55 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -163,6 +163,13 @@ void unknownOption() { expectThrows({"--", "input.elf", "output.elf"}, "unknown option: --"); } +void emptyPositionals() { + for (const auto& arguments : std::vector>{ + {"", "input.elf", "output.elf"}, {"input.elf", "", "output.elf"}, + {"", "output.elf"}, {"input.elf", ""}}) + expectThrows(arguments, "input and output paths must be nonempty"); +} + void tooManyPositionals() { expectThrows({"input.elf", "output.elf", "extra.elf"}, "unexpected argument: extra.elf"); } @@ -211,6 +218,7 @@ int main() { unusedFilterRejectsDuplicates(); unusedFilterBareWordIsUnknown(); unknownOption(); + emptyPositionals(); tooManyPositionals(); windowsDiagnosticsRequiresWindows(); windowsGuiRequiresWindows(); From 4de5726d32303d3ade07e9fb3e4f73c5151f91d9 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 15:14:33 -0300 Subject: [PATCH 181/318] fix(graphics): distinguish reused memory from depth storage aliases --- .../Graphics/include/DepthSurface.hpp | 2 ++ .../Graphics/src/DepthSurface.cpp | 18 ++++++++++++++++ .../Graphics/src/ShaderResources.cpp | 6 +++--- .../tests/DepthSurfaceReuse.cpp | 21 +++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index f5b6cad4a..a3f6c027d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -16,6 +16,8 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); +// Descriptor-aware lookup distinguishes a depth plane from temporary memory reused for color. +bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); void NoteHtileDepthClear(std::uint64_t htileAddress); bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 0b5d5eb79..781a4105b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -284,6 +284,24 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 } } +bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource) { + std::lock_guard lock(surfacesMutex()); + const auto& list = surfaces(); + const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { + return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + }); + if (found == list.rend()) return false; + // As in the sampled lookup, a previous depth allocation at the same address alone does not + // identify this descriptor. Games reuse temporary GPU memory for ordinary color textures. + const auto& target = (*found)->target; + if (resource.width != target.extent.width || resource.height != target.extent.height) return false; + const bool stencil = target.stencilAddress != 0 && target.stencilAddress == resource.baseAddress; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; + const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); + return ResolveTextureFormat(resource.format) == expected || depthBits; +} + bool DepthSurfaceAt(std::uint64_t address) { std::lock_guard lock(surfacesMutex()); return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 0383b7571..a45572e58 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -834,9 +834,9 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); - if (DepthSurfaceAt(viewed.baseAddress)) { - char text[112]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(viewed.baseAddress)); + if (DepthSurfaceAt(context, words, viewed)) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(viewed.baseAddress), viewed.width, viewed.height, viewed.format, mip, static_cast(viewed.dimension), static_cast(viewed.tileMode)); throw std::runtime_error(text); } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index 63ddb1ae1..bf83e852c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -186,5 +186,26 @@ void RunDepthSurfaceReuseTests() { Require(lookup(context, {stencil, FormatR8G8B8A8Unorm, 320, 180, TileRenderTarget64KB}) == nullptr, "an RGBA8 view of a stencil plane's own extent must be read as reused memory"); Require(lookup(context, {stencil, FormatR32Float, 320, 180}) == nullptr, "an R32F view of a stencil plane's own extent must be read as reused memory"); + const auto storageAliasesDepth = [&](const Context& owner, const View& view) { + const auto words = tsharp(view); + return DepthSurfaceAt(owner, words, DecodeTextureResource(words)); + }; + Require(storageAliasesDepth(context, {depth32, FormatR32Float, 384, 384}), "storage access to the actual D32 plane must remain rejected"); + Require(storageAliasesDepth(context, {depth16, FormatR16Uint, 256, 256}), "raw D16 storage access must remain rejected"); + Require(!storageAliasesDepth(context, {depth32, FormatR32Float, 128, 128}), "temporary memory reused at a different extent is not a depth storage alias"); + Require(!storageAliasesDepth(context, {depth32, FormatR11G11B10Float, 384, 384}), "temporary memory reused for a color format is not a depth storage alias"); + Require(!storageAliasesDepth(context, {depth32, FormatR32Uint, 384, 384, TileRenderTarget64KB}), "raw integer color memory without a depth layout is not a depth storage alias"); + auto otherDevice = context; + static int otherDeviceTag = 0; + otherDevice.device = reinterpret_cast(&otherDeviceTag); + Require(!storageAliasesDepth(otherDevice, {depth32, FormatR32Float, 384, 384}), "a depth surface on another device must not block a storage lookup"); + + constexpr std::uint64_t stencil16 = 0x60000000; + DepthSurfaceView(context, {depth16, stencil16, {128, 128}, VK_FORMAT_D16_UNORM_S8_UINT, 1.0f, 0}); + Require(!storageAliasesDepth(context, {depth16, FormatR16Unorm, 256, 256}), "an older depth extent at the same address must not override the newest surface"); + Require(storageAliasesDepth(context, {depth16, FormatR16Unorm, 128, 128}), "the newest depth extent must still reject actual storage access"); + Require(storageAliasesDepth(context, {stencil16, FormatR8Uint, 128, 128}), "storage access to the actual stencil plane must remain rejected"); + Require(!storageAliasesDepth(context, {stencil16, FormatR11G11B10Float, 128, 128}), "a stencil address reused for color is not a stencil storage alias"); + ClearDepthSurfaces(context.device); } From b9b83cef2b18c9f3df5da8680cd934eda2f4af40 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 15:34:38 -0300 Subject: [PATCH 182/318] style(graphics): follow the code comment convention --- core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp | 1 - core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp | 2 -- 2 files changed, 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index a3f6c027d..f27b19b65 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -16,7 +16,6 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); -// Descriptor-aware lookup distinguishes a depth plane from temporary memory reused for color. bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); void NoteHtileDepthClear(std::uint64_t htileAddress); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 781a4105b..9ceaca0cd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -291,8 +291,6 @@ bool DepthSurfaceAt(const Context& context, std::span words return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); }); if (found == list.rend()) return false; - // As in the sampled lookup, a previous depth allocation at the same address alone does not - // identify this descriptor. Games reuse temporary GPU memory for ordinary color textures. const auto& target = (*found)->target; if (resource.width != target.extent.width || resource.height != target.extent.height) return false; const bool stencil = target.stencilAddress != 0 && target.stencilAddress == resource.baseAddress; From ef218ef928ac83ba06e06c5ed9bf846c0c4bb490 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sun, 11 Oct 2026 09:31:33 -0300 Subject: [PATCH 183/318] test(agc): distinguish GPU depth from reused storage bytes --- .../tests/execution/DepthStorageReuse.cpp | 78 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 + 2 files changed, 80 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp new file mode 100644 index 000000000..91db433d8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp @@ -0,0 +1,78 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Width; + +alignas(4096) std::array Depth{}; +alignas(256) std::array Output{}; +alignas(256) constexpr std::array LoadCode{ + 0x7e3c0280u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, + 0xbf8c3f70u, 0xf0201108u, 0x00010a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array SampleCode{ + 0x7e3c02ffu, 0x3f000000u, 0x7e3e02ffu, 0x3f000000u, 0xf09c0108u, + 0x00610a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u, +}; + +void Read(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t width, bool sampled, float expected) { + Output.fill(0xdeadbeefu); + const auto output = reinterpret_cast(Output.data()); + const auto depth = reinterpret_cast(Depth.data()); + const std::array data{ + static_cast(output), static_cast(output >> 32u), sizeof(Output), 0x31016facu, + static_cast(depth >> 8u), static_cast(depth >> 40u) | (format << 20u) | (((width - 1u) & 3u) << 30u), + ((width - 1u) >> 2u) | ((Height - 1u) << 14u), 0x90000facu, 0u, 0u, 0u, 0u, + 0x92u, 0u, 0u, 0u, + }; + const auto code = sampled ? std::span(SampleCode) : std::span(LoadCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, data, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128}, + }; + request.useCache = false; + const auto shader = ShaderRecompiler::Recompile(request); + device.Dispatch(shader, 1, 1, 1); + device.WaitIdle(); + for (std::size_t lane = 0; lane < 32; ++lane) { + Require((sampled && format == 7u ? std::abs(std::bit_cast(Output[lane]) - expected) <= 1.0f / 65535.0f : Output[lane] == std::bit_cast(expected)), "depth reuse returned the wrong GPU value at lane " + std::to_string(lane) + ": got " + std::to_string(Output[lane]) + ", expected " + std::to_string(std::bit_cast(expected))); + } + Require(std::all_of(Depth.begin(), Depth.end(), [](float value) { return value == 0.25f; }), "depth reuse changed the guest bytes"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + const auto address = reinterpret_cast(Depth.data()); + for (const auto format : {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM}) { + AgcDriver::Graphics::ClearDepthSurfaces(device->Device()); + Depth.fill(0.25f); + const DepthFastClearHarness::Surface surface{address, 0, 0, false, format}; + Require(DepthFastClearHarness::Draw(*device, shaders, surface, {.depthWrite = true}) == DepthFastClearHarness::Covered, "depth producer did not cover the surface"); + Read(*device, format == VK_FORMAT_D16_UNORM ? 7u : 22u, Width, true, 0.5f); + Read(*device, 22u, format == VK_FORMAT_D16_UNORM ? Width : Width / 2u, false, 0.25f); + } + std::puts("depth storage reuse GPU tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..72dee2ce4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -371,6 +371,8 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [Reused depth storage](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. Matching depth/stencil storage access still throws. + - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. - [sem_timedwait](../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. From 1b4baaee2745287e1da22247c44747455b1ade93 Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 01:17:59 +0200 Subject: [PATCH 184/318] fix(memory): exclude arena host holes from every write-tracking query On Windows agc_write_tracking has failed since #3594 with write tracking test failed: an unwatched range reads as not stored over from the check in CheckSharedBlock: if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not stored over"); The host stack array lies in a host hole of the guest arena. #3594 made Watched, collectWrites, UnchangedSince, UnchangedSinceAll and storeOwn reject ranges overlapping such holes, but StoredOver, MarkWritten, WrittenSince, ChangedBlocks and ImportWatched still accepted them through WriteTracker::covers(), so StoredOver proved a range not stored over while Watched called it unwatched (#4087). The exclusion now lives in WriteTracker::covers() on Windows, which every query already asks, and the per-function checks are removed. Host holes are padded to the allocation granularity and the tracker blocks are 64 KiB from the arena base, so no block is shared between a hole and arena memory, and ranges outside the holes get the same answers as before. A new case enumerates the arena for a host hole and checks every query against it; on the previous code StoredOver, MarkWritten, StoreOwnBytes, WrittenSince and ChangedBlocks fail it. Fixes #4087 Co-Authored-By: Claude Fable 5.1 --- .../Execution/src/GuestMemory.cpp | 17 +------- .../libSceAgcDriver/tests/WriteTracking.cpp | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index de2fb32eb..112c4fe78 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -780,7 +780,7 @@ struct WriteTracker { bool covers(std::uint64_t address, std::size_t bytes) const { #ifdef _WIN32 - return coverage.Covers(address, bytes); + return coverage.Covers(address, bytes) && !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast(address), bytes); #else return address + bytes <= LeafCount * LeafBlocks * WriteBlockBytes && GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(static_cast(address), bytes); #endif @@ -1059,9 +1059,6 @@ bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop, std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoized) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return 0; -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return 0; -#endif auto& tracker = Tracker(); // Whole pages, so a page shared with the next range is collected with either. constexpr std::uint64_t page = 4096; @@ -1190,9 +1187,6 @@ bool WriteWatched() { bool Watched(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; -#endif auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1201,9 +1195,6 @@ bool Watched(std::uint64_t address, std::size_t bytes) { bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; -#endif auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1220,9 +1211,6 @@ bool UnchangedSinceAll(std::span queries) { if (!tracker.watched) return false; for (const auto& [address, bytes, generation] : queries) { if (generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return false; -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; -#endif const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(address + bytes - 1); if (!tracker.unchanged(first, last, generation)) return false; @@ -1249,9 +1237,6 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func constexpr std::uint64_t page = 4096; const auto first = address & ~(page - 1); const auto stop = (address + bytes + page - 1) & ~(page - 1); -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(first, static_cast(stop - first))) return stampStored(tracker, store()); -#endif if (!walkWrites(tracker, first, stop, StampKind::Cpu)) return stampStored(tracker, store()); const auto stored = store(); walkWrites(tracker, first, stop, StampKind::Driver); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index b924f0d5c..9ee69136d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -184,6 +184,47 @@ void CheckPrivateMappingReuse() { GuestArena::GuestArenaReset_nid_postfix(replacement, 3 * Block); GuestArena::GuestArenaRelease_nid_postfix(replacement, 3 * Block); } + +std::uint64_t ArenaHostHole() { + std::uintptr_t arena = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); + for (auto cursor = arena; cursor < arena + arenaBytes;) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == sizeof(info), "cannot enumerate arena host holes"); + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(cursor, 1)) return cursor; + const auto next = reinterpret_cast(info.BaseAddress) + info.RegionSize; + Require(next > cursor, "arena enumeration did not advance"); + cursor = next; + } + throw std::runtime_error("the write-watched arena contains no host hole to test"); +} + +void CheckHostHole() { + constexpr std::size_t bytes = 64; + const auto hole = ArenaHostHole(); + const auto before = TrackerGeneration(); + Require(!Watched(hole, bytes), "an arena host hole reads as watched"); + Require(CollectWrites(hole, bytes) == 0 && CollectWritesUncached(hole, bytes) == 0, "an arena host hole was collected"); + Require(StoredOver(hole, bytes, before), "an arena host hole reads as not stored over"); + Require(!UnchangedSince(hole, bytes, before), "an arena host hole reads as unchanged"); + const std::array queries{UnchangedQuery{hole, bytes, before}}; + Require(!UnchangedSinceAll(queries), "an arena host hole reads as unchanged in a batch"); + Require(!UnchangedSinceCollected(hole, bytes, before), "an arena host hole reads as unchanged since a collect"); + Require(MarkWritten(hole, bytes) == 0, "a driver store into an arena host hole is stamped"); + Require(StoreOwnBytes(hole, bytes, [] {}) == 0, "an own store into an arena host hole is stamped"); + Require(!WrittenSince(hole, bytes, before), "an arena host hole reads its stamps as written"); + std::array generations{before}; + std::array changed{}; + std::array cpu{}; + Require(!ChangedBlocks(hole, bytes, generations, changed, cpu) && changed[0] == BlockWritten && cpu[0] == 1, "an arena host hole uses block stamps"); + bool imported = false; + Require(ImportWatched(hole, bytes, [&] { + imported = true; + return true; + }) && imported, "an import into an arena host hole did not run"); + Require(!Watched(hole, bytes), "an import made an arena host hole watched"); +} #endif void CheckVersionRanges() { @@ -312,6 +353,7 @@ int main() { CheckUnwatch(); #ifdef _WIN32 CheckPrivateMappingReuse(); + CheckHostHole(); #endif CheckVersionRanges(); CheckCollectBoundaries(); From 8938a257f2e2d46095cc1b78c91447f4cf5f5833 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 13:44:23 +1300 Subject: [PATCH 185/318] test(agc): read lane 63 of the vertex index in the registered workgroup fixture The fixture read lane 63 of v0, which the program never writes. Since #3546 a wave64 vertex program on a narrower host subgroup builds that read as 0, so the synchronous registration the test expects to fail succeeds. v5 holds the vertex index, which is not 0 in lanes 32-63, so the read still throws. --- .../prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 4aa54af7d..ed91e5570 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -531,7 +531,7 @@ void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool return; } alignas(256) static constexpr std::array code{ - 0xd8340000u, 0u, 0xd7600000u, 0x00017f00u, 0x7e020200u, 0xf80008cfu, 0x04030201u, 0xbf810000u + 0xd8340000u, 0u, 0xd7600000u, 0x00017f05u, 0x7e020200u, 0xf80008cfu, 0x04030201u, 0xbf810000u }; struct Header { Shader shader{}; From 8f9efc25f211ff94f88bfcde147b6a93f9143246 Mon Sep 17 00:00:00 2001 From: winstrex <79346005+winstrex@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:29:29 -0300 Subject: [PATCH 186/318] fix(libs): take guest entropy from the host OS instead of std::random_device std::random_device in libstdc++ reads RDSEED on x86 Linux. On an AMD EPYC 9V45 (Zen 5) CI runner its 32-bit results repeat about 130 times in 65536 draws, so guest_posix_random fails. libc now exports HostEntropy_nid_no_patch, which uses getentropy (rand_s on Windows) and throws when the OS cannot provide entropy. arc4random, arc4random_buf, sceRandomGetRandomNumber, /dev/random and /dev/urandom reads, std::_Random_device and sceKernelUuidCreate use it, and the arc4random and random device TechnicalDebt entries say so. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePosixForWebKit/Export.cpp | 10 ++---- core/libs/prx/libSceRandom/Export.cpp | 11 ++----- core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/include/HostEntropy.hpp | 8 +++++ core/libs/prx/libc/src/HostEntropy.cpp | 33 +++++++++++++++++++ core/libs/prx/libc/src/RuntimeSupport.cpp | 9 +++-- .../prx/libkernel/File/src/RandomDevice.cpp | 11 ++----- .../libs/prx/libkernel/System/src/Process.cpp | 7 ++-- core/libs/tests/GuestAppInfo.cpp | 9 +++++ core/libs/tests/GuestCrt.cpp | 6 ++++ docs/dev/TechnicalDebt.md | 4 +-- 11 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 core/libs/prx/libc/include/HostEntropy.hpp create mode 100644 core/libs/prx/libc/src/HostEntropy.cpp diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index 0c3b4b758..4ff202b86 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -1,10 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include +#include "prx/libc/include/HostEntropy.hpp" #include #include #include #include -#include #include namespace { @@ -172,12 +171,7 @@ char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle) { } void APS5_VABI arc4random_buf_nid_postfix(void* buffer, std::size_t size) { - static thread_local std::random_device device; - auto* bytes = static_cast(buffer); - for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) { - const unsigned int value = device(); - std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset)); - } + HostEntropy_nid_no_patch(buffer, size); } std::uint32_t APS5_VABI arc4random_nid_postfix() { diff --git a/core/libs/prx/libSceRandom/Export.cpp b/core/libs/prx/libSceRandom/Export.cpp index 4e9ef2674..c61ffc4d5 100644 --- a/core/libs/prx/libSceRandom/Export.cpp +++ b/core/libs/prx/libSceRandom/Export.cpp @@ -1,10 +1,8 @@ #include #include -#include -#include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/HostEntropy.hpp" namespace { @@ -17,12 +15,7 @@ extern "C" { int APS5_VABI sceRandomGetRandomNumber(void* buf, size_t size) { if (!buf || size > SCE_RANDOM_MAX_SIZE) return SCE_RANDOM_ERROR_INVALID; - static thread_local std::random_device device; - auto* bytes = static_cast(buf); - for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) { - const unsigned int value = device(); - std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset)); - } + HostEntropy_nid_no_patch(buf, size); return 0; } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d68768c32..b3830d4cc 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/StringExtensions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Random.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StdioExtras.cpp diff --git a/core/libs/prx/libc/include/HostEntropy.hpp b/core/libs/prx/libc/include/HostEntropy.hpp new file mode 100644 index 000000000..3380f9023 --- /dev/null +++ b/core/libs/prx/libc/include/HostEntropy.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTENTROPY_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTENTROPY_HPP + +#include + +extern "C" void HostEntropy_nid_no_patch(void* buffer, std::size_t size); + +#endif diff --git a/core/libs/prx/libc/src/HostEntropy.cpp b/core/libs/prx/libc/src/HostEntropy.cpp new file mode 100644 index 000000000..d87362a22 --- /dev/null +++ b/core/libs/prx/libc/src/HostEntropy.cpp @@ -0,0 +1,33 @@ +#ifdef _WIN32 +#define _CRT_RAND_S +#endif +#include "prx/libc/include/HostEntropy.hpp" + +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include +#elif !defined(_WIN32) +#include +#endif + +void HostEntropy_nid_no_patch(void* buffer, std::size_t size) { + auto* bytes = static_cast(buffer); +#ifdef _WIN32 + for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) { + unsigned int value = 0; + if (const errno_t error = rand_s(&value); error != 0) + throw std::system_error(error, std::generic_category(), "HostEntropy: rand_s failed"); + std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset)); + } +#else + constexpr std::size_t MaxChunk = 256; + for (std::size_t offset = 0; offset < size; offset += MaxChunk) { + if (getentropy(bytes + offset, std::min(MaxChunk, size - offset)) != 0) + throw std::system_error(errno, std::generic_category(), "HostEntropy: getentropy failed"); + } +#endif +} diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 69be0d848..e984aaee5 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #ifdef _WIN32 @@ -20,6 +19,7 @@ #include "prx/libc/include/SonyCrt.hpp" #include "prx/libc/include/specifics/gcc/AtomicOps.hpp" #include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/HostEntropy.hpp" namespace { @@ -123,10 +123,9 @@ std::uintmax_t APS5_VABI strtoumax_nid_postfix(const char* str, char** endptr, i } unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() { - static std::random_device device; - static std::mutex mutex; - std::lock_guard lock(mutex); - return device(); + unsigned int value = 0; + HostEntropy_nid_no_patch(&value, sizeof(value)); + return value; } [[noreturn]] void APS5_VABI _ZSt14_Throw_C_errori_nid_postfix(int code) { diff --git a/core/libs/prx/libkernel/File/src/RandomDevice.cpp b/core/libs/prx/libkernel/File/src/RandomDevice.cpp index 1edaddd69..1f4cfdda9 100644 --- a/core/libs/prx/libkernel/File/src/RandomDevice.cpp +++ b/core/libs/prx/libkernel/File/src/RandomDevice.cpp @@ -1,9 +1,7 @@ #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libc/include/HostEntropy.hpp" -#include -#include #include -#include #include #ifdef _WIN32 @@ -47,12 +45,7 @@ bool IsRandomDevice(int fd) { bool ReadRandomDevice(int fd, void* buf, std::size_t nbytes) { std::lock_guard lock(g_mutex); if (!g_descriptors.contains(fd)) return false; - static std::random_device device; - auto* bytes = static_cast(buf); - for (std::size_t offset = 0; offset < nbytes; offset += sizeof(std::uint32_t)) { - const std::uint32_t value = device(); - std::memcpy(bytes + offset, &value, nbytes - offset < sizeof(value) ? nbytes - offset : sizeof(value)); - } + HostEntropy_nid_no_patch(buf, nbytes); return true; } diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index f4d546837..f9215dcdd 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -13,6 +13,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/HostEntropy.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" @@ -29,7 +30,6 @@ #include #include #include -#include #include #include #include @@ -342,11 +342,8 @@ void* APS5_VABI sceKernelGetProcParam(void) { int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid) { if (!uuid) return sceInvalidArgument; - static thread_local std::random_device device; - std::uniform_int_distribution distribution; std::array value; - for (auto& word : value) - word = distribution(device); + HostEntropy_nid_no_patch(value.data(), sizeof(value)); value[1] = (value[1] & 0x0fffffffu) | 0x40000000u; value[2] = (value[2] & 0xffffff3fu) | 0x80u; std::memcpy(uuid, value.data(), sizeof(value)); diff --git a/core/libs/tests/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index e39662613..37b7cff39 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -10,9 +10,11 @@ extern "C" { int APS5_VABI sceKernelGetAppInfo(int pid, void* info); int APS5_VABI getpid_nid_postfix(void); +int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid); } static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static void Require(bool value) { if (!value) std::abort(); } @@ -65,4 +67,11 @@ int main(int argc, char** argv) { #endif Require(std::system(command.c_str()) != 0); std::filesystem::remove_all(empty); + + Require(sceKernelUuidCreate(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::uint32_t first[4]{}; + std::uint32_t second[4]{}; + Require(sceKernelUuidCreate(first) == 0 && sceKernelUuidCreate(second) == 0); + for (const std::uint32_t* uuid : {first, second}) Require((uuid[1] >> 28) == 4u && (uuid[2] & 0xc0u) == 0x80u); + Require(std::memcmp(first, second, sizeof(first)) != 0); } diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index 7bd72ca01..611ddc32a 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -33,6 +33,7 @@ int APS5_VABI __fpclassifyd_nid_postfix(double value); unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor); SonyLconv* APS5_VABI localeconv_nid_postfix(void); int APS5_VABI sceAmprFpclassifyd(double value); +unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix(); } #define Require(condition) do { if (!(condition)) { std::fprintf(stderr, "guest_crt: line %d: %s\n", __LINE__, #condition); std::abort(); } } while (0) @@ -145,4 +146,9 @@ int main() { Require(sceAmprMbstowcs(nullptr, "ABC", 8) == 3); Require(sceAmprMbstowcs(wide, "ABC", 2) == 2); Require(mbstowcs_nid_postfix(wide, "ABC", 8) == 3); + + const unsigned int firstRandom = _ZSt14_Random_devicev_nid_postfix(); + bool randomChanged = false; + for (int draw = 0; draw < 8; ++draw) randomChanged |= _ZSt14_Random_devicev_nid_postfix() != firstRandom; + Require(randomChanged); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..4d8a5b4b7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -381,7 +381,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. - [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. -- [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes from the same thread-local host `std::random_device` as `arc4random_buf`, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent +- [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws - [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning - [_Cnd_signal, _Cnd_init_with_name](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known @@ -405,7 +405,7 @@ Throughout the project, every function at every stage either **does exactly what - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies -- [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws +- [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. From cded9ba8c373f0b149fd335e2ac8f73267531aeb Mon Sep 17 00:00:00 2001 From: saxopa <99569665+saxopa@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:12:02 +0200 Subject: [PATCH 187/318] fix(relinker): accept RELATIVE relocations against a zero-value section symbol OpenOrbis create-fself adds one to the symbol index of every relocation it copies from .rela.dyn, so each R_X86_64_RELATIVE entry of a homebrew built with the OpenOrbis toolchain names symbol 1, a local STT_SECTION symbol with value 0. FreeBSD rtld computes RELATIVE as base + addend without reading the symbol. The pipeline, the code analysis and the strict filter now accept index 0 or a STT_SECTION symbol with value 0, and still throw for any other symbol. The relocation is written with symbol index 0, where the pipeline previously dropped RELATIVE entries with a nonzero index. Co-Authored-By: Claude Opus 5.5 --- .../src/analysis/CodeInstructionCollector.cpp | 3 +- .../UnusedNidFilter/StrictNidFilter.cpp | 7 +++- .../src/pipeline/RelinkerPipeline.cpp | 14 +++++-- .../relinker/tests/StrictNidFilterTests.cpp | 26 +++++++++++++ .../relinker/tests/test_none_relocations.py | 38 +++++++++++++++---- 5 files changed, 76 insertions(+), 12 deletions(-) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 3d78157a2..876fa53ea 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -181,7 +181,8 @@ std::set CodeInstructionCollector::Collect(const std::ve const auto index = info >> 32; const auto addend = Io::ReadU64(bytes, offset + 16); if (type == 8) { - if (index != 0 || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); + const auto zeroSectionSymbol = index != 0 && index < symbolCount && (bytes[symbols + index * 24 + 4] & 15) == 3 && Io::ReadU64(bytes, symbols + index * 24 + 8) == 0; + if ((index != 0 && !zeroSectionSymbol) || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); addRoot(addend); } else if (type == 1 || type == 6 || type == 7) { if (index == 0 || index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index cf163ac55..3eede671e 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -79,7 +79,12 @@ public: const auto slot = read(*rela + offset); const auto info = read(*rela + offset + 8); if (static_cast(info) != 8) continue; - if (info >> 32) throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + if (const auto symbol = info >> 32; symbol != 0) { + const auto symbols = table(6, 0x61000039); + const auto size = tags.find(0x6100003F); + if (!symbols || size == tags.end() || symbol >= size->second / 24 || (read(*symbols + symbol * 24 + 4) & 15) != 3 || read(*symbols + symbol * 24 + 8) != 0) + throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + } const auto target = read(*rela + offset + 16); if (input.ImportSlots.contains(slot) || !input.Pointers.emplace(slot, target).second) throw RelinkerException("Strict filter: overlapping pointer relocations", slot); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index a486d3d64..d1ff243f4 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -219,6 +219,15 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return relaOff + off; }; + auto isZeroSectionSymbol = [&](const std::uint32_t symIdx) { + const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize) + throw RelinkerException("Symbol table entry out of bounds", symOff); + std::uint64_t value = 0; + std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); + return (raw[symOff + 4] & 0xf) == 3 && value == 0; + }; + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); @@ -236,7 +245,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf continue; if (relType == 8) { - if (symIdx != 0) + if (symIdx != 0 && !isZeroSectionSymbol(symIdx)) throw RelinkerException("RELATIVE relocation has a nonzero symbol index", pos); continue; } @@ -405,9 +414,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&rOffset, raw.data() + pos, 8); std::memcpy(&rInfo, raw.data() + pos + 8, 8); std::memcpy(&rAddend, raw.data() + pos + 16, 8); - const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); - if (symIdx == 0 && relType == R_X86_64_RELATIVE) + if (relType == R_X86_64_RELATIVE) appendRela(dynSection.RelaData, rOffset, static_cast(R_X86_64_RELATIVE), rAddend); } }; diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index f62ec3f09..4d3f25755 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -576,6 +576,32 @@ void sceDynlibDataOffsets() { "Duplicate PT_SCE_DYNLIBDATA segments were accepted"); requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, duplicate, input.Text, input.TextVaddr); }, "Strict filter accepted duplicate PT_SCE_DYNLIBDATA segments"); + + auto sectionRelative = bytes; + write(sectionRelative, 208, 0x70); + write(sectionRelative, 216, 0x70); + write(sectionRelative, 0x330, 0x61000039); + write(sectionRelative, 0x338, 0x100); + write(sectionRelative, 0x340, 0x6100003B); + write(sectionRelative, 0x348, 24); + write(sectionRelative, 0x350, 0x6100003F); + write(sectionRelative, 0x358, 48); + write(sectionRelative, 0x61C, 3); + write(sectionRelative, 0x908, (std::uint64_t{1} << 32) | 8); + require(Relinker::MakeStrictUnusedNidFilter()->Filter(references, sectionRelative, input.Text, input.TextVaddr).size() == 1, + "Strict filter rejected a RELATIVE relocation against a zero-value section symbol"); + auto valuedSection = sectionRelative; + write(valuedSection, 0x620, 0x10); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, valuedSection, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a section symbol with a value"); + auto untyped = sectionRelative; + write(untyped, 0x61C, 0); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, untyped, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a non-section symbol"); + auto outside = sectionRelative; + write(outside, 0x908, (std::uint64_t{2} << 32) | 8); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, outside, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation symbol outside the symbol table"); } } diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 5a0d06fc3..cbf3d4cb3 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -79,13 +79,37 @@ def main(): if target == "windows" and os.name == "nt" and not has_import: run = subprocess.run([str(output)], capture_output=True, timeout=20) assert run.returncode == 42, (name, level, run.returncode, run.stderr) - source = work / "invalid-relative.elf" - source.write_bytes(image_with_relocations([noop, (0x300, (1 << 32) | 8, 0x210)], False, False)) - output = work / "invalid-relative.exe" - result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], - capture_output=True, text=True, timeout=20) - assert result.returncode == 2 and "RELATIVE relocation has a nonzero symbol index" in result.stderr - assert not output.exists() + for sce in (False, True): + image = image_with_relocations([noop, (0x300, (1 << 32) | 8, 0x210)], sce, False) + struct.pack_into(" Date: Mon, 12 Oct 2026 00:44:54 +0200 Subject: [PATCH 188/318] fix(agc): accept a CB metadata pass beside depth state without depth or stencil planes --- .../prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- .../libs/prx/libSceAgcDriver/tests/Graphics.cpp | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 3316fd778..981c8dee3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1004,7 +1004,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue if (mode != 2u && mode != 6u && !resolve) return std::nullopt; ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma"); - Require((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0, "CB metadata pass with depth or stencil work"); + Require(depthPlanesAbsent(cx) || ((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0), "CB metadata pass with depth or stencil work"); const auto samples = resolve ? decodeSamples(cx) : 1u; if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x80, ~0u, "window offset"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 0103b5aec..c0b2cc36e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1434,6 +1434,23 @@ void metadataPassTests() { queue.context[0x202] = 0xcc0060; queue.context[0x200] = 2; expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); + queue.context[0x10] = 0x80000180; + queue.context[0x11] = 0x20000180; + queue.context[0x200] = 0x700763; + queue.context[0x0] = 0x20; + Require(DecodeColorMetadataPass(queue).has_value(), "depth and stencil state without depth or stencil planes refused the pass"); + queue.context[0x10] = 0x80000183; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); + queue.context[0x10] = 0x80000180; + queue.context[0x11] = 0x20000181; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); + queue.context[0x11] = 0x20000180; + queue.context.erase(0x200); + queue.context.erase(0x0); + Require(DecodeColorMetadataPass(queue).has_value(), "unwritten depth and render control refused a pass without depth or stencil planes"); + queue.context.erase(0x10); + queue.context.erase(0x11); + queue.context[0x0] = 0; queue.context[0x200] = 0x70; Require(DecodeColorMetadataPass(queue).has_value(), "an always-pass depth function without a test refused the pass"); queue.context[0x91] = 0x40020; From b843b16a0a9fb79b0820e740c4403195ed50dbef Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:11:42 +0200 Subject: [PATCH 189/318] fix(libSceSystemService): answer parameter 400 with the console language --- core/libs/prx/libSceSystemService/Export.cpp | 3 ++- .../libs/prx/libSceSystemService/SystemService.hpp | 1 + core/libs/tests/GuestSystemServiceLanguage.cpp | 14 +++++++++++--- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index da7831be4..fc2b46ac9 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -92,7 +92,8 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { return SYSTEM_SERVICE_ERROR_PARAMETER; } switch (paramId) { - case SYSTEM_SERVICE_PARAM_ID_LANG: *value = ConsoleLanguage(); break; + case SYSTEM_SERVICE_PARAM_ID_LANG: + case SYSTEM_SERVICE_PARAM_ID_400: *value = ConsoleLanguage(); break; case SYSTEM_SERVICE_PARAM_ID_DATE_FORMAT: *value = SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY; break; case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break; case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = 0; break; diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index 01f6f34f2..99b73cc0b 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -17,6 +17,7 @@ constexpr int SYSTEM_SERVICE_PARAM_ID_TIME_ZONE = 4; constexpr int SYSTEM_SERVICE_PARAM_ID_SUMMERTIME = 5; constexpr int SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME = 6; constexpr int SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL = 7; +constexpr int SYSTEM_SERVICE_PARAM_ID_400 = 400; constexpr int SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN = 1000; constexpr std::size_t SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH = 65; diff --git a/core/libs/tests/GuestSystemServiceLanguage.cpp b/core/libs/tests/GuestSystemServiceLanguage.cpp index 4b0e67f4d..efd37f499 100644 --- a/core/libs/tests/GuestSystemServiceLanguage.cpp +++ b/core/libs/tests/GuestSystemServiceLanguage.cpp @@ -21,20 +21,27 @@ void SetLanguage(const char* value) { int Language() { int value = -1; Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, &value) == SYSTEM_SERVICE_OK); + int sameValue = -1; + Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_400, &sameValue) == SYSTEM_SERVICE_OK); + Require(sameValue == value); return value; } -bool Rejected(const char* value) { - SetLanguage(value); +bool ParameterRejected(int paramId) { int language = -1; try { - static_cast(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, &language)); + static_cast(sceSystemServiceParamGetInt(paramId, &language)); } catch (const std::runtime_error& error) { return language == -1 && std::string(error.what()).find("ANYPS5_LANGUAGE") != std::string::npos; } return false; } +bool Rejected(const char* value) { + SetLanguage(value); + return ParameterRejected(SYSTEM_SERVICE_PARAM_ID_LANG) && ParameterRejected(SYSTEM_SERVICE_PARAM_ID_400); +} + } int main() { @@ -54,6 +61,7 @@ int main() { SetLanguage("arabic"); Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_400, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); int enterButton = -1; Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN, &enterButton) == SYSTEM_SERVICE_OK); Require(enterButton == SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..8be9a265a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -296,7 +296,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the meaning of the value is unknown; 0 is reported, as for the unset game presets. PPSA12544 (Unity) only stores it for its user profile - [sceGameUpdateGetAddcontLatestVersion](../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw -- [sceSystemServiceParamGetInt](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed +- [sceSystemServiceParamGetInt](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) are throwing stubs. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`. No report is produced yet, so registering a handler cannot be honoured. PPSA04540 imports both and registers a CPU load handler (type 5) - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` - [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero From b45232dda67ccea86b19f5b7a24a1b0944a01f72 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sun, 11 Oct 2026 22:18:20 -0300 Subject: [PATCH 190/318] test(memory): handle Windows layouts without arena host holes --- core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 9ee69136d..b3ecb5b81 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -197,12 +197,16 @@ std::uint64_t ArenaHostHole() { Require(next > cursor, "arena enumeration did not advance"); cursor = next; } - throw std::runtime_error("the write-watched arena contains no host hole to test"); + return 0; } void CheckHostHole() { constexpr std::size_t bytes = 64; const auto hole = ArenaHostHole(); + if (hole == 0) { + std::cout << "host-hole query checks skipped: the arena contains no host hole\n"; + return; + } const auto before = TrackerGeneration(); Require(!Watched(hole, bytes), "an arena host hole reads as watched"); Require(CollectWrites(hole, bytes) == 0 && CollectWritesUncached(hole, bytes) == 0, "an arena host hole was collected"); From 4b125c0ca5f2492ecd693072d8e43eefabeaa6df Mon Sep 17 00:00:00 2001 From: pallp Date: Mon, 12 Oct 2026 02:18:33 +0100 Subject: [PATCH 191/318] fix(savedata): fill the search infos of sceSaveDataDirNameSearch SaveDataDirNameSearchResult.infos points at an array the title reads one entry per returned directory name, but the search never wrote it, and SaveDataSearchInfo was declared as 128 opaque bytes. A title that asks for the infos read whatever its buffer held. SaveDataSearchInfo is now the 48-byte record shadPS4 declares as OrbisSaveDataSearchInfo (blocks, free blocks, 32 reserved bytes), and the search fills one per returned name. Every native save reports SAVE_DATA_BLOCKS_MAX blocks, as sceSaveDataGetMountInfo and the block sort keys already assume, so both counts are SAVE_DATA_BLOCKS_MAX and the reserved bytes are zero. guest_savedata_search_tests checks the two records, their zero reserved bytes and that the search writes nothing past them. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VFVE5WbaUJ2vyj8NAW44CS --- core/libs/SceTypes.hpp | 7 ++++++- core/libs/prx/libSceSaveData.native/Export.cpp | 3 +++ core/libs/tests/GuestSaveDataSearch.cpp | 12 ++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 4269cee09..29dddbc5a 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1448,7 +1448,12 @@ struct SaveDataMountInfo { struct SceSaveDataTitleId { char data[10]; char pad[2]; }; struct SceSaveDataDirName { char data[32]; }; -struct SaveDataSearchInfo { std::uint8_t opaque[128]; }; +struct SaveDataSearchInfo { + std::uint64_t blocks; + std::uint64_t free_blocks; + std::uint8_t reserved[32]; +}; +static_assert(sizeof(SaveDataSearchInfo) == 48); struct SaveDataMemoryData { void* buf; std::size_t buf_size; std::size_t offset; }; struct SaveDataMount3 { diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 89fd8a121..a2c9cc131 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -315,6 +315,9 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if (result->params != nullptr) { result->params[i] = sortByParam ? matches[i].param : load_param(matches[i].path); } + if (result->infos != nullptr) { + result->infos[i] = SaveDataSearchInfo{SAVE_DATA_BLOCKS_MAX, SAVE_DATA_BLOCKS_MAX, {}}; + } } result->hit_num = static_cast(matches.size()); result->set_num = set; diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 0be7b2d33..601783fe8 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -92,11 +92,23 @@ static void SearchAndMount() { SaveDataDirNameSearchResult result{}; result.dir_names = reinterpret_cast(buffer.data()); result.dir_names_num = 2; + alignas(SaveDataSearchInfo) std::array infos; + infos.fill(0x21); + result.infos = reinterpret_cast(infos.data()); Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "search failed"); Require(result.hit_num == 2 && result.set_num == 2, "search must return both saves"); for (std::size_t i = 64; i < buffer.size(); ++i) { Require(buffer[i] == '!', "directory search overran two 32-byte guest records"); } + for (std::size_t i = 96; i < infos.size(); ++i) { + Require(infos[i] == 0x21, "directory search overran two 48-byte search infos"); + } + for (std::size_t i = 0; i < 2; ++i) { + SaveDataSearchInfo info{}; + std::memcpy(&info, infos.data() + i * 48, sizeof(info)); + Require(info.blocks == 32768 && info.free_blocks == 32768, "search info must report the save's blocks and free blocks"); + for (const auto byte : info.reserved) Require(byte == 0, "search info reserved bytes must be zero"); + } std::array found{}; for (int i = 0; i < 2; ++i) { const char* record = buffer.data() + i * 32; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..4df622c51 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -303,7 +303,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet - [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw -- [sceSaveDataDirNameSearch](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it +- [sceSaveDataDirNameSearch](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`) report `SAVE_DATA_BLOCKS_MAX` for both. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it - [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature; returns `EOPNOTSUPP` like `sceTextToSpeech2Initialize` - [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature From 7d9966a8ee730127d7cc43a9370f3950d3eba365 Mon Sep 17 00:00:00 2001 From: Nixitov Date: Sun, 11 Oct 2026 23:51:54 +0200 Subject: [PATCH 192/318] feat(libc): implement __divti3 Kart PS5 (PPSA99611) imports __divti3 and stops at start-up on main. The guest has no signed 128-bit division; add it alongside __udivti3, dividing the magnitudes and negating the quotient, and throwing on a zero divisor like the unsigned routine. --- core/libs/prx/libc/include/SonyCrt.hpp | 13 ++++++++++++ core/libs/prx/libc/src/RuntimeSupport.cpp | 4 ++++ core/libs/tests/GuestCrt.cpp | 24 +++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index 2016da156..cc544b21c 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -194,6 +194,19 @@ inline unsigned __int128 SonyUdivti3(unsigned __int128 dividend, unsigned __int1 return quotient; } +inline __int128 SonyDivti3(__int128 dividend, __int128 divisor) { + if (divisor == 0) throw std::runtime_error("__divti3: integer divide by zero"); + const bool negative = (dividend < 0) != (divisor < 0); + const unsigned __int128 magnitudeDividend = dividend < 0 + ? (unsigned __int128)0 - (unsigned __int128)dividend + : (unsigned __int128)dividend; + const unsigned __int128 magnitudeDivisor = divisor < 0 + ? (unsigned __int128)0 - (unsigned __int128)divisor + : (unsigned __int128)divisor; + const unsigned __int128 magnitude = SonyUdivti3(magnitudeDividend, magnitudeDivisor); + return (__int128)(negative ? (unsigned __int128)0 - magnitude : magnitude); +} + } #endif diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 69be0d848..6731cbb4c 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -181,6 +181,10 @@ void APS5_VABI _Unlocksyslock_nid_postfix() { g_sysLock.unlock(); } +__int128 APS5_VABI __divti3_nid_postfix(__int128 dividend, __int128 divisor) { + return SonyDivti3(dividend, divisor); +} + unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor) { return SonyUdivti3(dividend, divisor); } diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index 7bd72ca01..10257c096 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -31,6 +31,7 @@ std::size_t APS5_VABI mbstowcs_nid_postfix(std::uint16_t* dest, const char* src, char* APS5_VABI strtok_s_nid_postfix(char* str, const char* delim, char** context); int APS5_VABI __fpclassifyd_nid_postfix(double value); unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor); +__int128 APS5_VABI __divti3_nid_postfix(__int128 dividend, __int128 divisor); SonyLconv* APS5_VABI localeconv_nid_postfix(void); int APS5_VABI sceAmprFpclassifyd(double value); } @@ -84,6 +85,29 @@ int main() { } Require(trapped); + Require(__divti3_nid_postfix(100, 3) == 33); + Require(__divti3_nid_postfix(-100, 3) == -33); + Require(__divti3_nid_postfix(100, -3) == -33); + Require(__divti3_nid_postfix(-100, -3) == 33); + Require(SonyDivti3(0, 5) == 0); + const __int128 large = (__int128)1 << 100; + Require(SonyDivti3(large, 3) == large / 3); + Require(SonyDivti3(-large, 3) == -(large / 3)); + Require(SonyDivti3(large, -3) == -(large / 3)); + Require(SonyDivti3(-large, -3) == large / 3); + const __int128 minimum = (__int128)((unsigned __int128)1 << 127); + const __int128 maximum = (__int128)(((unsigned __int128)1 << 127) - 1); + Require(SonyDivti3(minimum, 1) == minimum); + Require(SonyDivti3(minimum, -1) == minimum); + Require(SonyDivti3(maximum, -1) == -maximum); + trapped = false; + try { + SonyDivti3(1, 0); + } catch (const std::exception&) { + trapped = true; + } + Require(trapped); + void* previous = sceAmprSetConstraintHandlerS(reinterpret_cast(&CountingHandler)); Require(previous != nullptr); Require(previous != reinterpret_cast(&CountingHandler)); From 910363657dfeeb0bb21a11f9616ff34ffe318938 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 11 Oct 2026 10:29:22 -0700 Subject: [PATCH 193/318] fix(libc): count the whole string in mbstowcs with a null destination mbstowcs, and libSceAmpr's copy of it, counted at most n characters when the destination was NULL, so the usual sizing call mbstowcs(NULL, s, 0) returned 0 and mbstowcs(NULL, s, 1) returned 1 for a three-character string. A caller that sizes its buffer that way got a buffer too small for the string and no terminator. POSIX says that with a null pwcs mbstowcs "shall return the length required to convert the entire array regardless of the value of n", and FreeBSD does that: lib/libc/locale/mbstowcs.c passes SIZE_T_MAX as the source limit, and the C locale's _none_mbsnrtowcs (none.c) returns the distance to the terminating NUL when "dst == NULL". With a null destination, SonyMbstowcs now returns the length of the whole string. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/include/SonyCrt.hpp | 3 ++- core/libs/tests/GuestCrt.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index 2016da156..56fe7b6a1 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -125,10 +125,11 @@ inline int SonyWcscpyS(std::uint16_t* dest, std::size_t size, const std::uint16_ inline std::size_t SonyMbstowcs(std::uint16_t* dest, const char* src, std::size_t size) { if (!src) return 0; + if (!dest) return std::strlen(src); std::size_t converted = 0; while (converted < size) { const auto value = static_cast(src[converted]); - if (dest) dest[converted] = value; + dest[converted] = value; if (value == 0) return converted; ++converted; } diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index 611ddc32a..f37cb68bb 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -144,6 +144,8 @@ int main() { Require(sceAmprMbstowcs(wide, "ABC", 8) == 3); Require(wide[0] == 65 && wide[1] == 66 && wide[2] == 67 && wide[3] == 0); Require(sceAmprMbstowcs(nullptr, "ABC", 8) == 3); + Require(sceAmprMbstowcs(nullptr, "ABC", 0) == 3 && sceAmprMbstowcs(nullptr, "ABC", 1) == 3); + Require(mbstowcs_nid_postfix(nullptr, "ABC", 0) == 3 && mbstowcs_nid_postfix(nullptr, "ABC", 1) == 3); Require(sceAmprMbstowcs(wide, "ABC", 2) == 2); Require(mbstowcs_nid_postfix(wide, "ABC", 8) == 3); From cba3b5a8b38ce972d03f700f108e99175f4d8ce4 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 11 Oct 2026 10:30:44 -0700 Subject: [PATCH 194/318] fix(libkernel): accept SOCK_NONBLOCK and SOCK_CLOEXEC in socket socket only took a type of exactly SOCK_STREAM or SOCK_DGRAM, so a type with SOCK_NONBLOCK (0x20000000) or SOCK_CLOEXEC (0x10000000) set failed with EPROTONOSUPPORT, although socketpair already accepted both. FreeBSD strips both flags before creating the socket (sys/kern/ uipc_syscalls.c, kern_socket: "if ((type & SOCK_CLOEXEC) != 0) { type &= ~SOCK_CLOEXEC; oflag |= O_CLOEXEC; }" and the same for SOCK_NONBLOCK with FNONBLOCK), and makes the new socket non-blocking with "fo_ioctl(fp, FIONBIO, &fflag, ...)". socket now does the same as socketpair: it creates the socket from the plain type and, with SOCK_NONBLOCK, switches it to non-blocking. SOCK_CLOEXEC needs nothing more, since a guest never executes another program. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 11 +++++++---- core/libs/tests/GuestUdp.cpp | 10 ++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 52f37a428..55e2a03dc 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -487,9 +487,11 @@ int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, return 0; } int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { + const bool nonblocking = (type & GuestNonBlocking) != 0; + const int plainType = type & ~(GuestCloseOnExec | GuestNonBlocking); if (family != 2 && family != 28) return Fail(47); - const int nativeType = type == 1 ? SOCK_STREAM : type == 2 ? SOCK_DGRAM : -1; - const int expectedProtocol = type == 1 ? 6 : 17; + const int nativeType = plainType == 1 ? SOCK_STREAM : plainType == 2 ? SOCK_DGRAM : -1; + const int expectedProtocol = plainType == 1 ? 6 : 17; if (nativeType == -1 || (protocol != 0 && protocol != expectedProtocol)) return Fail(43); #ifdef _WIN32 static const int startup = [] { WSADATA data{}; return WSAStartup(MAKEWORD(2, 2), &data); }(); @@ -497,10 +499,11 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { #endif const auto native = ::socket(family == 2 ? AF_INET : AF_INET6, nativeType, protocol); if (native == Invalid) return Fail(NativeError()); - Socket guard(native, family, type); + Socket guard(native, family, plainType); try { - auto socket = std::make_shared(native, family, type); + auto socket = std::make_shared(native, family, plainType); guard.value = Invalid; + if (nonblocking && SetNonBlocking(*socket)) return -1; std::lock_guard lock(socketsMutex); if (nextDescriptor == INT_MAX) return Fail(24); const int descriptor = nextDescriptor++; diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 2f69a2201..69386d2db 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -163,4 +163,14 @@ int main() { const int stream = socket_nid_postfix(2, 1, 0); Require(stream >= 0); Require(close_nid_postfix(stream) == 0); + const int nonblocking = socket_nid_postfix(2, 2 | 0x20000000, 0); + Require(nonblocking >= 0 && fcntl_nid_postfix(nonblocking, 3) == 6); + std::array nonblockingAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(nonblocking, nonblockingAddress.data(), nonblockingAddress.size()) == 0); + Require(recvfrom_nid_postfix(nonblocking, buffer, sizeof(buffer), 0, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + Require(close_nid_postfix(nonblocking) == 0); + const int closeOnExec = socket_nid_postfix(2, 1 | 0x10000000, 6); + Require(closeOnExec >= 0 && fcntl_nid_postfix(closeOnExec, 3) == 2); + Require(close_nid_postfix(closeOnExec) == 0); + Require(socket_nid_postfix(2, 0x20000000, 0) == -1 && *__error_nid_postfix() == 43); } From 30d1e85283d9822b428e31791e62ba8a01c46f9a Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 11 Oct 2026 22:16:58 +0000 Subject: [PATCH 195/318] fix(libScePosixForWebKit): use FreeBSD's gai_strerror message texts gai_strerror returned its own wording for most codes, so a resolver failure was described with text FreeBSD's libc does not use: EAI_NONAME (8) gave "Name or service not known" instead of "hostname nor servname provided, or not known", EAI_BADFLAGS (3) "Invalid flags" instead of "Invalid value for ai_flags", and an unknown code "Unknown address resolution error" instead of "Unknown error". Twelve of the fifteen texts differed. FreeBSD 11.4, lib/libc/net/gai_strerror.c: static const char *ai_errlist[] = { "Success", /* 0 */ "Address family for hostname not supported", /* 1 */ "Temporary failure in name resolution", /* EAI_AGAIN */ "Invalid value for ai_flags", /* EAI_BADFLAGS */ "Non-recoverable failure in name resolution", /* EAI_FAIL */ "ai_family not supported", /* EAI_FAMILY */ "Memory allocation failure", /* EAI_MEMORY */ "No address associated with hostname", /* 7 */ "hostname nor servname provided, or not known", /* EAI_NONAME */ "servname not supported for ai_socktype", /* EAI_SERVICE */ "ai_socktype not supported", /* EAI_SOCKTYPE */ "System error returned in errno", /* EAI_SYSTEM */ "Invalid value for hints", /* EAI_BADHINTS */ "Resolved protocol is unknown", /* EAI_PROTOCOL */ "Argument buffer overflow" /* EAI_OVERFLOW */ }; ... if (ecode >= 0 && ecode < EAI_MAX) return ai_errlist[ecode]; return "Unknown error"; The table and the unknown-code text now match, as 5830d8e44 did for strerror. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePosixForWebKit/Resolver.cpp | 10 +++++----- core/libs/tests/GuestResolver.cpp | 9 +++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 7c5254fee..a1d4d64a1 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -284,13 +284,13 @@ int APS5_VABI getnameinfo_nid_postfix(const void* address, std::uint32_t length, const char* APS5_VABI gai_strerror_nid_postfix(int error) { static const char* messages[] = { "Success", "Address family for hostname not supported", "Temporary failure in name resolution", - "Invalid flags", "Name resolution failed", "Address family not supported", - "Memory allocation failed", "No address for hostname", "Name or service not known", - "Service not supported for socket type", "Socket type not supported", "System error", - "Invalid hints", "Unknown protocol", "Buffer too small" + "Invalid value for ai_flags", "Non-recoverable failure in name resolution", "ai_family not supported", + "Memory allocation failure", "No address associated with hostname", "hostname nor servname provided, or not known", + "servname not supported for ai_socktype", "ai_socktype not supported", "System error returned in errno", + "Invalid value for hints", "Resolved protocol is unknown", "Argument buffer overflow" }; if (error < 0 || error >= static_cast(sizeof(messages) / sizeof(messages[0]))) - return "Unknown address resolution error"; + return "Unknown error"; return messages[error]; } } diff --git a/core/libs/tests/GuestResolver.cpp b/core/libs/tests/GuestResolver.cpp index 33bbed3d4..20f02e1d1 100644 --- a/core/libs/tests/GuestResolver.cpp +++ b/core/libs/tests/GuestResolver.cpp @@ -113,8 +113,13 @@ int main() { Require(getnameinfo_nid_postfix(v4.data(), 2, host, sizeof(host), nullptr, 0, 10) == 5); Require(getnameinfo_nid_postfix(v4.data(), v4.size(), host, sizeof(host), nullptr, 0, 32) == 3); Require(getnameinfo_nid_postfix(v4.data(), v4.size(), host, 1, nullptr, 0, 10) == 14); - Require(std::strlen(gai_strerror_nid_postfix(14)) > 0); - Require(std::strstr(gai_strerror_nid_postfix(-1), "Unknown") != nullptr); + Require(std::strcmp(gai_strerror_nid_postfix(0), "Success") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(3), "Invalid value for ai_flags") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(8), "hostname nor servname provided, or not known") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(10), "ai_socktype not supported") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(14), "Argument buffer overflow") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(-1), "Unknown error") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(15), "Unknown error") == 0); GuestResolver::AddressInfo hints{}; hints.flags = 12; // numeric host and service: no external DNS dependency hints.socketType = 1; From 589de27c1782916ea31342c476e31fd50fdf9dfe Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 11 Oct 2026 09:20:58 -0700 Subject: [PATCH 196/318] fix(libc): accept the fdopen modes FreeBSD accepts fdopen only took a fixed list of 15 mode strings and failed with EINVAL for any other, so "rt", "r+t", "re", "rbe" or "rw" gave NULL. FreeBSD's fdopen parses the mode with __sflags (lib/libc/stdio/flags.c): after r, w or a it takes b, +, x, e and v and stops at the first other character ("default: known = 0;"), and only a bad first character or x on a read-only mode fail with EINVAL. fopen and freopen already parse modes this way through ParseFileMode (#2033). fdopen now uses ParseFileMode as well and passes the host its normalised mode without x, which FreeBSD's fdopen ignores. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/FileAndHeap.cpp | 13 +++++----- core/libs/tests/GuestStandardStreams.cpp | 30 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 87c352e2b..8ef29c8fc 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -60,18 +60,17 @@ extern "C" { FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { if (descriptor < 0) { errno = 9; return nullptr; } if (!mode) { errno = 22; return nullptr; } - const char* supported[] = {"r", "w", "a", "rb", "wb", "ab", "r+", "w+", "a+", - "rb+", "wb+", "ab+", "r+b", "w+b", "a+b"}; - bool valid = false; - for (const auto* candidate : supported) if (std::strcmp(mode, candidate) == 0) valid = true; - if (!valid) { errno = 22; return nullptr; } + const auto parsed = ParseFileMode(mode); + if (!parsed) { errno = 22; return nullptr; } try { + auto nativeMode = parsed->native; + std::erase(nativeMode, 'x'); const auto release = [](void* pointer) { ::operator delete(pointer); }; std::unique_ptr storage(::operator new(sizeof(FileStream)), release); #ifdef _WIN32 - auto* native = ::_fdopen(descriptor, mode); + auto* native = ::_fdopen(descriptor, nativeMode.c_str()); #else - auto* native = ::fdopen(descriptor, mode); + auto* native = ::fdopen(descriptor, nativeMode.c_str()); #endif if (!native) return nullptr; auto* stream = new (storage.get()) FileStream(native, true); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 5de08e2cb..191491421 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -312,6 +312,35 @@ static bool CheckBinaryModes() { return correct; } +static int DuplicateDescriptor(std::FILE* stream) { +#ifdef _WIN32 + return _dup(_fileno(stream)); +#else + return ::dup(::fileno(stream)); +#endif +} + +static void CheckDescriptorModes(std::FILE* original) { + for (const char* mode : {"rt", "r+t", "re", "rbe", "rw", "r+x"}) { + const int descriptor = DuplicateDescriptor(original); + Require(descriptor >= 0); + auto* wrapped = fdopen_nid_postfix(descriptor, mode); + Require(wrapped != nullptr && fseek_nid_postfix(wrapped, 0, SEEK_SET) == 0); + char contents[32]{}; + Require(fgets_nid_postfix(contents, sizeof(contents), wrapped) == contents && std::strcmp(contents, "retained") == 0); + Require(fclose_nid_postfix(wrapped) == 0); + } + const int descriptor = DuplicateDescriptor(original); + Require(descriptor >= 0); + *__error_nid_postfix() = 0; + Require(fdopen_nid_postfix(descriptor, "rx") == nullptr && *__error_nid_postfix() == 22); +#ifdef _WIN32 + Require(_close(descriptor) == 0); +#else + Require(::close(descriptor) == 0); +#endif +} + int main() { Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); @@ -339,6 +368,7 @@ int main() { #else Require(::close(descriptor) == -1); #endif + CheckDescriptorModes(original); Require(std::fclose(original) == 0); char stringOutput[256]; std::memset(stringOutput, '!', sizeof(stringOutput)); From b41c0b3da9b2726cf4b04aab33f7cb3f676812a6 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Sun, 11 Oct 2026 00:35:09 -0700 Subject: [PATCH 197/318] fix(libkernel): reject oversized Windows directory entry names (cherry picked from commit b0e0c1c4bfd1487047873663115a94a46987b831) --- .../File/src/DirectoryDescriptor.cpp | 4 ++ core/libs/tests/GuestDirectoryBuffer.cpp | 62 +++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 9bbe6569b..01a3170d7 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -126,6 +126,10 @@ int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { std::size_t used = 0; while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; + if (name.size() > 255) { + if (used == 0) return SCE_KERNEL_ERROR_ENAMETOOLONG; + break; + } const std::size_t record = (8 + name.size() + 1 + 3) & ~std::size_t{3}; if (used + record > static_cast(nbytes)) { if (used == 0) return SCE_KERNEL_ERROR_EINVAL; diff --git a/core/libs/tests/GuestDirectoryBuffer.cpp b/core/libs/tests/GuestDirectoryBuffer.cpp index 9f2bf60d0..6d1e6f4c3 100644 --- a/core/libs/tests/GuestDirectoryBuffer.cpp +++ b/core/libs/tests/GuestDirectoryBuffer.cpp @@ -17,6 +17,9 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelGetdents(int, char*, int); int APS5_VABI sceKernelGetdirentries(int, char*, int, std::int64_t*); +int APS5_VABI getdents_nid_postfix(int, char*, int); +int APS5_VABI getdirentries_nid_postfix(int, char*, int, std::int64_t*); +int* APS5_VABI __error_nid_postfix(); } static void Check(bool value, int line) { @@ -27,15 +30,15 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) -int main() { +static void CheckName(const std::u8string& utf8Name) { const auto root = std::filesystem::temp_directory_path() / ("anyps5-directory-buffer-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); - const std::string name = "a-long-directory-entry-name.bin"; - { std::ofstream file(root / name); Require(static_cast(file)); } + const std::string name(utf8Name.begin(), utf8Name.end()); + { std::ofstream file(root / std::filesystem::path(utf8Name)); Require(static_cast(file)); } const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); - std::array buffer; + std::array buffer; buffer.fill('x'); std::int64_t base = -1; Require(sceKernelGetdirentries(directory, buffer.data(), 8, &base) == SCE_KERNEL_ERROR_EINVAL); @@ -54,9 +57,60 @@ int main() { const int recordSize = static_cast((8 + name.size() + 1 + 3) & ~std::size_t{3}); Require(sceKernelGetdirentries(directory, buffer.data(), recordSize, &base) == recordSize); Require(std::strcmp(buffer.data() + 8, name.c_str()) == 0); + Require(static_cast(buffer[7]) == name.size()); + std::uint16_t storedSize = 0; + std::memcpy(&storedSize, buffer.data() + 4, sizeof(storedSize)); + Require(storedSize == recordSize); + const auto rootUtf8 = root.u8string(); + const std::string enumeratedPath = std::string(rootUtf8.begin(), rootUtf8.end()) + '/' + (buffer.data() + 8); + const int file = sceKernelOpen(enumeratedPath.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0 && sceKernelClose(file) == 0); Require(sceKernelGetdents(directory, buffer.data(), 8) == 0); Require(sceKernelGetdents(directory, buffer.data(), 256) == 0); Require(sceKernelClose(directory) == 0); Require(sceKernelGetdents(-1, buffer.data(), 256) == SCE_KERNEL_ERROR_ENOTDIR); std::filesystem::remove_all(root); } + +static void CheckOversizedName() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-name-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + std::u8string name; + for (int i = 0; i < 86; ++i) name += u8"\u30bb"; + { std::ofstream file(root / std::filesystem::path(name)); Require(static_cast(file)); } + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + std::array buffer; + buffer.fill('x'); + Require(sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())) == 24); + Require(std::strcmp(buffer.data() + 8, ".") == 0); + Require(std::strcmp(buffer.data() + 20, "..") == 0); + Require(std::all_of(buffer.begin() + 24, buffer.end(), [](char byte) { return byte == 'x'; })); + for (int attempt = 0; attempt < 2; ++attempt) { + buffer.fill('x'); + Require(sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())) == SCE_KERNEL_ERROR_ENAMETOOLONG); + Require(std::all_of(buffer.begin(), buffer.end(), [](char byte) { return byte == 'x'; })); + } + *__error_nid_postfix() = 0; + Require(getdents_nid_postfix(directory, buffer.data(), static_cast(buffer.size())) == -1); + Require(*__error_nid_postfix() == 63); + *__error_nid_postfix() = 0; + std::int64_t base = -1; + Require(getdirentries_nid_postfix(directory, buffer.data(), static_cast(buffer.size()), &base) == -1); + Require(*__error_nid_postfix() == 63); + Require(std::all_of(buffer.begin(), buffer.end(), [](char byte) { return byte == 'x'; })); + Require(sceKernelClose(directory) == 0); + std::filesystem::remove_all(root); +} + +int main() { + CheckName(u8"a-long-directory-entry-name.bin"); + CheckName(u8"caf\u00e9-music.bin"); + CheckName(u8"\u30bb\u30fc\u30d6-music.bin"); + CheckName(u8"\U0001f3ae-music.bin"); + std::u8string longest; + for (int i = 0; i < 85; ++i) longest += u8"\u30bb"; + CheckName(longest); + CheckOversizedName(); +} From 20b801b7e866525a6114332336dae71377077c75 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 14:51:21 +1300 Subject: [PATCH 198/318] test(agc): report the packed f16 native skip as a device limitation, as check_skips expects --- .../prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp index 092a17fd7..e1455fa12 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp @@ -148,7 +148,7 @@ int main() { return std::ranges::find(target.supportedCapabilities, static_cast(capability)) != target.supportedCapabilities.end(); }; if (!supports(spv::CapabilityFloat16) || !supports(spv::CapabilityRoundingModeRTE) || !supports(spv::CapabilityDenormPreserve)) { - std::puts("packed f16 native: the device has no native f16 with RTE rounding and kept denormals, skipped"); + std::puts("packed f16 native: skipped, the device has no native f16 with RTE rounding and kept denormals"); return VulkanTestSkipped; } std::vector withoutFloat16; From ce9fc689b70d42a26c599881e6d44c06659d3639 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Mon, 12 Oct 2026 01:51:35 +0000 Subject: [PATCH 199/318] docs(libkernel): record Windows directory name limit --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4d8a5b4b7..d923f952c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -404,7 +404,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceHttpAbortWaitRequest, sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the abort takes only the epoll handle, as in [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), not confirmed on a PS5 title or hardware. An abort stays set: every later wait on the handle, a poll included, returns `SCE_HTTP_ERROR_ABORTED` (0x80431080) until the handle is destroyed. A wait woken by `sceHttpDestroyEpoll`, or by the `sceHttpTerm` that ends the last context, returns 0 events. The epoll calls return `BEFORE_INIT` (0x80431001) while no `sceHttpInit` context is alive and `INVALID_ID` (0x80431100) for an unknown context or handle, and `sceHttpTerm` returns the same errors instead of succeeding for any id. Handles are pointers, so a stale handle whose address a newer epoll reuses is taken as that epoll - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown -- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. From 6b2432875dac620ad358834d8781a18289033fb2 Mon Sep 17 00:00:00 2001 From: Achraf Reyani Date: Mon, 12 Oct 2026 01:46:19 +0900 Subject: [PATCH 200/318] fix(agc): accept a linear 2D texture whose DEPTH holds its default pitch On GFX10.3 the DEPTH field of a 1D or 2D non-array linear T# carries the row pitch minus one (Mesa's ac_descriptors.c sets it for custom pitches, which must be multiples of 256 bytes). A single-level, uncompressed 2D linear texture whose DEPTH equals its default 256-byte-aligned pitch now decodes as that 2D texture; any other pitch still throws. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestTextureResource.cpp | 7 +++++++ .../tests/GuestTextureResource.cpp | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 8fea5aec0..8e854983e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include #include #include @@ -152,6 +153,12 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(height == 1 && depth == 0 && baseArray == 0, "guest 1D texture descriptor has a nonzero height, depth or base array"); break; case TextureDimension::k2D: + if (depth != 0 && tileMode == TextureTileMode::kLinear) { + const auto elementsPerPitchUnit = 256u / BytesPerElement(format); + const auto defaultPitch = (width + elementsPerPitchUnit - 1u) / elementsPerPitchUnit * elementsPerPitchUnit; + Require(!IsBlockCompressed(format) && maxMip == 0 && baseArray == 0 && depth + 1u == defaultPitch, "guest 2D linear texture descriptor sets a custom pitch other than its default pitch, which is not implemented" + describe()); + depth = 0; + } Require(depth == 0 && baseArray == 0, "guest 2D texture descriptor has a nonzero depth or base array"); break; case TextureDimension::k3D: diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 0539db1b3..a605c6cd6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -160,7 +160,26 @@ void RunGuestTextureResourceTests() { Fields twoD = base; twoD.depth = 1; + rejectFields(twoD, "custom pitch other than its default pitch"); + twoD.tileModeRaw = 0x09; rejectFields(twoD, "nonzero depth or base array"); + Fields pitched = base; + pitched.width = 72; + pitched.height = 72; + pitched.depth = 127; + result = DecodeTextureResource(pack(pitched)); + Require(result.dimension == TextureDimension::k2D && result.width == 72 && result.depthOrLastArray == 0, "linear 2D descriptor with its default pitch in DEPTH decoded incorrectly"); + pitched.width = 8; + pitched.height = 8; + pitched.depth = 63; + result = DecodeTextureResource(pack(pitched)); + Require(result.width == 8 && result.depthOrLastArray == 0, "linear 2D descriptor with a one-unit default pitch decoded incorrectly"); + pitched.depth = 127; + rejectFields(pitched, "custom pitch other than its default pitch"); + pitched.depth = 63; + pitched.maxMip = 1; + pitched.lastLevel = 1; + rejectFields(pitched, "custom pitch other than its default pitch"); twoD = base; twoD.baseArray = 1; rejectFields(twoD, "nonzero depth or base array"); From 572e9d9f784c80ad0a193c63ce209308e25fcc87 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 15:06:12 +1300 Subject: [PATCH 201/318] test(agc): word the tessellation and oversized dispatch skips as device limitations, as check_skips expects --- .../prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp index e6bf9cfdf..068e0f31c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp @@ -181,7 +181,7 @@ int main() { } else if (device->WorkGroupCountLimit()[0] < Groups) { CheckSplitCoverage(*device); } else { - std::printf("oversized dispatch split not tested: the device's %u workgroup limit never splits a %ux1x1 dispatch\n", device->WorkGroupCountLimit()[0], Groups); + std::printf("oversized dispatch split: skipped, the device reports a %u workgroup limit, which never splits a %ux1x1 dispatch\n", device->WorkGroupCountLimit()[0], Groups); return VulkanTestSkipped; } AgcDriverShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 5453e3c05..0e991c307 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -317,7 +317,7 @@ int main() { if (!device) return VulkanTestSkipped; const auto target = device->Target(); if (!target.tessellation.has_value()) { - std::puts("Tessellation tests skipped: the device has no tessellation shaders"); + std::puts("tessellation: skipped, the device has no tessellation shaders"); return VulkanTestSkipped; } ExpectRefusal(target, ShaderStage::TessellationControl, HullUnguardedFactors, "not written by control point 0 alone"); From 29a843579d5b0214e3553720ec10e83ab52df0ea Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sun, 11 Oct 2026 23:27:02 -0300 Subject: [PATCH 202/318] test(memory): query host holes beside warm collect memos --- .../prx/libSceAgcDriver/tests/WriteTracking.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index b3ecb5b81..5489cac1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -210,6 +210,22 @@ void CheckHostHole() { const auto before = TrackerGeneration(); Require(!Watched(hole, bytes), "an arena host hole reads as watched"); Require(CollectWrites(hole, bytes) == 0 && CollectWritesUncached(hole, bytes) == 0, "an arena host hole was collected"); + auto* watched = static_cast(AllocateWatched(Block, true)); + struct Cleanup { + void* memory; + ~Cleanup() { GuestArena::GuestArenaRelease_nid_postfix(memory, Block); } + } cleanup{watched}; + const auto base = reinterpret_cast(watched); + for (unsigned epoch = 0; epoch < 2; ++epoch) { + BumpCollectEpoch(); + watched[7] ^= 1; + Require(CollectWrites(base + 7, 1) != 0, "a warm host-hole check could not collect its watched range"); + for (unsigned repeat = 0; repeat < 3; ++repeat) { + Require(CollectWrites(base + 7, 1) != 0, "a warm host-hole check lost its watched range"); + Require(CollectWrites(hole, bytes) == 0, "a repeated host-hole query hit a collect memo"); + Require(CollectWrites(hole + 7, 1) == 0, "a page-rounded host-hole query hit a collect memo"); + } + } Require(StoredOver(hole, bytes, before), "an arena host hole reads as not stored over"); Require(!UnchangedSince(hole, bytes, before), "an arena host hole reads as unchanged"); const std::array queries{UnchangedQuery{hole, bytes, before}}; From 4b378a251f2e3e3487ff6efc0b9338ebaf2ae3b0 Mon Sep 17 00:00:00 2001 From: overlordxrz-source Date: Mon, 12 Oct 2026 02:39:50 +0000 Subject: [PATCH 203/318] feat(libSceRudp): implement sceRudpGetMaxSegmentSize --- core/libs/prx/libSceRudp/Export.cpp | 12 +++++++++-- core/libs/tests/GuestRudpPump.cpp | 31 ++++++++++++++++++++--------- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index cff5949a7..bbd31b09a 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -25,6 +25,7 @@ constexpr int RUDP_POOL_MIN_SIZE = 0xC58; constexpr std::uintptr_t RUDP_POOL_ALIGNMENT = 8; constexpr std::size_t RUDP_STATUS_SIZE = 0xF8; constexpr std::size_t RUDP_STATUS_CURRENT_CONTEXTS = 0x64; +constexpr uint16_t RUDP_MAX_SEGMENT_SIZE = 1410; using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); @@ -273,8 +274,15 @@ int APS5_VABI sceRudpNetReceived(void) { return 0; } -int APS5_VABI sceRudpGetMaxSegmentSize(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (size == nullptr) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + *size = RUDP_MAX_SEGMENT_SIZE; return 0; } diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 36cf63cb4..62a2bd9c8 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -11,7 +11,7 @@ int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpTerminate(); int APS5_VABI sceRudpProcessEvents(); int APS5_VABI sceRudpNetFlush(); -int APS5_VABI sceRudpGetMaxSegmentSize(); +int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); int APS5_VABI sceRudpNetReceived(); } @@ -25,23 +25,36 @@ static void Require(bool value, int line) { int main() { constexpr int notInitialized = static_cast(0x80770001); + constexpr int invalidArgument = static_cast(0x80770004); + std::uint16_t segmentSize = 0x5a5a; CHECK(sceRudpProcessEvents() == notInitialized); CHECK(sceRudpNetFlush() == notInitialized); + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); + CHECK(segmentSize == 0x5a5a); + CHECK(sceRudpGetMaxSegmentSize(nullptr) == notInitialized); alignas(8) static unsigned char pool[0xC58]; CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); CHECK(sceRudpProcessEvents() == 0); CHECK(sceRudpNetFlush() == 0); + CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == 0); + CHECK(segmentSize == 1410); + std::uint16_t segment[3] = {0, 0, 0x5a5a}; + CHECK(sceRudpGetMaxSegmentSize(&segment[1]) == 0); + CHECK(segment[0] == 0 && segment[1] == 1410 && segment[2] == 0x5a5a); CHECK(sceRudpTerminate() == 0); - for (const auto call : {&sceRudpNetReceived, &sceRudpGetMaxSegmentSize}) { - bool threw = false; - try { - call(); - } catch (const std::runtime_error&) { - threw = true; - } - CHECK(threw); + segmentSize = 0x5a5a; + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); + CHECK(segmentSize == 0x5a5a); + + bool threw = false; + try { + sceRudpNetReceived(); + } catch (const std::runtime_error&) { + threw = true; } + CHECK(threw); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 32bf8fa29..87c0a434c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -73,6 +73,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNpEntitlementAccessGetEntitlementKey](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified - [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfo{,List}](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. The single-item variants copy the behaviour of the list ones, with no source of their own. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified - [sceRudpGetStatus](../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. +- [sceRudpGetMaxSegmentSize](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written - [sceHttpSetCookieEnabled](../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified - [sceHmd2Initialize](../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero From 5959506bbda5f12ec69910cf6121d313f7664b9a Mon Sep 17 00:00:00 2001 From: Yassine Date: Sat, 10 Oct 2026 14:55:41 +0000 Subject: [PATCH 204/318] test(avplayer): link guest_avplayer with the shared runtime libraries guest_avplayer_tests imports __cxa_throw from libc.prx, which is loaded before libstdc++, but -static-libgcc gave it a private copy of _Unwind_Resume. A failed Check threw through libc.prx's unwinder and the landing pad of the test function resumed with the private one, so on Linux every failure aborted in _Unwind_RaiseException_Phase2 instead of printing its message from main. This is the unexplained Linux abort in the intermittent guest_avplayer failures. The test is now linked without the static runtime flags, as the AgcDriver, Agc and VideoOut tests already are, so the unwinder comes from the same library as __cxa_throw. A forced Check failure in TestPlayback aborted before and now prints its message and exits with 1. (cherry picked from commit fa5199cd8fef7f9c7ab9e83a441e15d16364a81d) --- core/libs/prx/libSceAvPlayer/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceAvPlayer/CMakeLists.txt b/core/libs/prx/libSceAvPlayer/CMakeLists.txt index e2e599427..bdc296e7e 100644 --- a/core/libs/prx/libSceAvPlayer/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer/CMakeLists.txt @@ -2,6 +2,9 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/AvPlayerLibrary.cmake) add_sce_avplayer_library(libSceAvPlayer) if(BUILD_TESTING) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() add_test_executable(guest_avplayer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAvPlayer.cpp) target_include_directories(guest_avplayer_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_avplayer_tests PRIVATE libSceAvPlayer libc) From e0eef6c57fc221ce339bb0c5c07aebf19c59ce81 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:27:28 +0200 Subject: [PATCH 205/318] feat(pad): hold the sticks from the controller command file --- .../libScePad/include/InputCommandFile.hpp | 47 +++++++++++++++---- core/libs/prx/libScePad/src/PadState.cpp | 25 +++++----- core/libs/tests/GuestPad.cpp | 46 +++++++++++++++++- docs/user/USAGE.md | 4 +- tools/pad_input.py | 12 ++++- 5 files changed, 109 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index 8915531f4..7433fd9f4 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -1,6 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP #define CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP +#include +#include #include #include #include @@ -9,28 +11,38 @@ namespace Pad { class InputCommandFile { public: + struct Input { + std::uint32_t buttons = 0; + bool hasSticks = false; + std::array sticks{}; + + bool operator==(const Input&) const = default; + }; + explicit InputCommandFile(const char* path) : path(path != nullptr ? path : "") { if (!this->path.empty()) lastSequence = read().sequence; } - std::uint32_t Buttons(std::uint64_t now) { - if (path.empty()) return 0; + Input Poll(std::uint64_t now) { + if (path.empty()) return {}; if (now >= nextPoll) { nextPoll = now + 100000; const auto command = read(); if (command.sequence > lastSequence) { lastSequence = command.sequence; - buttons = command.buttons; + input = command.input; until = now + command.durationMs * 1000ull; } } - return now < until ? buttons : 0; + return now < until ? input : Input{}; } + std::uint32_t Buttons(std::uint64_t now) { return Poll(now).buttons; } + private: struct Command { std::uint64_t sequence = 0; - std::uint32_t buttons = 0; + Input input; std::uint32_t durationMs = 0; }; @@ -41,10 +53,27 @@ private: const std::string text(line); if (text.find('-') != std::string::npos) return {}; std::istringstream fields(text); + std::string tokens[8]; + int count = 0; + while (count < 8 && fields >> tokens[count]) ++count; + if (count != 3 && count != 7) return {}; + std::uint64_t values[7]{}; + for (int i = 0; i < count; ++i) { + const char* first = tokens[i].data() + (tokens[i].front() == '+' ? 1 : 0); + const char* last = tokens[i].data() + tokens[i].size(); + const auto parsed = std::from_chars(first, last, values[i]); + if (first == last || parsed.ec != std::errc{} || parsed.ptr != last) return {}; + } + if (values[0] == 0 || (values[1] & ~0x10fffeull) != 0 || values[2] == 0 || values[2] > 5000) return {}; Command command; - std::string extra; - if (!(fields >> command.sequence >> command.buttons >> command.durationMs) || fields >> extra) return {}; - if (command.sequence == 0 || (command.buttons & ~0x10fffeu) != 0 || command.durationMs == 0 || command.durationMs > 5000) return {}; + command.sequence = values[0]; + command.input.buttons = static_cast(values[1]); + command.durationMs = static_cast(values[2]); + for (int i = 3; i < count; ++i) { + if (values[i] > 255) return {}; + command.input.sticks[i - 3] = static_cast(values[i]); + } + command.input.hasSticks = count == 7; return command; } @@ -52,7 +81,7 @@ private: std::uint64_t lastSequence = 0; std::uint64_t nextPoll = 0; std::uint64_t until = 0; - std::uint32_t buttons = 0; + Input input; }; } diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index ca01712ee..9d8adf4c3 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -109,15 +109,16 @@ void Pad::Initialize() { } namespace { -PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, std::uint32_t injected) { +PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, const Pad::InputCommandFile::Input& injected) { PadData data{}; - data.buttons = state.buttons | injected; - data.left_stick_x = state.sticks[0]; - data.left_stick_y = state.sticks[1]; - data.right_stick_x = state.sticks[2]; - data.right_stick_y = state.sticks[3]; + data.buttons = state.buttons | injected.buttons; + const auto& sticks = injected.hasSticks ? injected.sticks : state.sticks; + data.left_stick_x = sticks[0]; + data.left_stick_y = sticks[1]; + data.right_stick_x = sticks[2]; + data.right_stick_y = sticks[3]; const auto trigger = [&](std::uint8_t analog, std::uint32_t bit) -> std::uint8_t { - return (injected & bit) != 0 || (analog == 0 && (state.buttons & bit) != 0) ? 255 : analog; + return (injected.buttons & bit) != 0 || (analog == 0 && (state.buttons & bit) != 0) ? 255 : analog; }; data.analog_buttons_l2 = trigger(state.analogButtonsL2, 0x100); data.analog_buttons_r2 = trigger(state.analogButtonsR2, 0x200); @@ -175,10 +176,10 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin return data; } -std::uint32_t InjectedButtons(std::uint64_t now) { +Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { static Pad::InputCommandFile commands(std::getenv("APS5_PAD_INPUT_FILE")); - static std::uint32_t lastInjected = 0; - const auto injected = commands.Buttons(now); + static Pad::InputCommandFile::Input lastInjected; + const auto injected = commands.Poll(now); if (injected != lastInjected) { lastInjected = injected; timestamp = now; @@ -192,7 +193,7 @@ PadData Pad::ReadState() { if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); - const std::uint32_t injected = InjectedButtons(now); + const auto injected = InjectedInput(now); return BuildData(state, timestamp, now, injected); } @@ -202,7 +203,7 @@ int Pad::Read(PadData* data, int num) { if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); - const std::uint32_t injected = InjectedButtons(now); + const auto injected = InjectedInput(now); if (queue.empty()) { data[0] = BuildData(state, timestamp, now, injected); return 1; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index a74a4bab5..4eb1d8b0b 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -57,8 +57,34 @@ static void CheckInputCommands() { Require(commands.Buttons(1001000) == 0); write("2 16384 200"); Require(commands.Buttons(1101000) == 0); + write("5 0 300 0 128 255 64"); + const auto tilted = commands.Poll(1201000); + Require(tilted.buttons == 0 && tilted.hasSticks); + Require(tilted.sticks[0] == 0 && tilted.sticks[1] == 128 && tilted.sticks[2] == 255 && tilted.sticks[3] == 64); + Require(!commands.Poll(1501000).hasSticks); + write("6 16384 300"); + const auto pressed = commands.Poll(1601000); + Require(pressed.buttons == 0x4000 && !pressed.hasSticks); + const Pad::InputCommandFile::Input none; + write("7 16384 300 128 128"); + Require(commands.Poll(1901000) == none); + write("7 16384 300 128 128 128 256"); + Require(commands.Poll(2001000) == none); + write("7 16384 300 128 128 128 128 128"); + Require(commands.Poll(2101000) == none); + write("7 16384 300 128 128 128 up"); + Require(commands.Poll(2201000) == none); + write("7 16384 300 +"); + Require(commands.Poll(2301000) == none); + write("7 16384 300 4294967296"); + Require(commands.Poll(2401000) == none); + write("7 16384 300 128 128 128 128x"); + Require(commands.Poll(2501000) == none); + write("7 8192 300 1 2 3 4"); + const auto accepted = commands.Poll(2601000); + Require(accepted.buttons == 0x2000 && accepted.hasSticks && accepted.sticks[3] == 4); std::filesystem::remove(path); - Require(commands.Buttons(1201000) == 0); + Require(commands.Buttons(3001000) == 0); Pad::InputCommandFile disabled(nullptr); Require(disabled.Buttons(0) == 0); } @@ -173,6 +199,24 @@ static void CheckInjectedState(const std::filesystem::path& path) { const auto released = Pad::ReadState(); Require(released.buttons == 0x2000 && released.analog_buttons_l2 == 0); Require(released.timestamp > pressed.timestamp); + physical.sticks[3] = 17; + PadPublishInput_nid_postfix(physical); + const auto still = Pad::ReadState(); + std::ofstream(path) << "2 0 300 200 0 128 128\n"; + PadData moved{}; + const auto stickDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + moved = Pad::ReadState(); + if (moved.left_stick_x == 200) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } while (std::chrono::steady_clock::now() < stickDeadline); + Require(moved.left_stick_x == 200 && moved.left_stick_y == 0); + Require(moved.right_stick_x == 128 && moved.right_stick_y == 128); + Require(moved.buttons == 0x2000 && moved.timestamp > still.timestamp); + std::this_thread::sleep_for(std::chrono::milliseconds(310)); + const auto centred = Pad::ReadState(); + Require(centred.left_stick_x == 42 && centred.right_stick_y == 17); + Require(centred.timestamp > moved.timestamp); PadPublishInput_nid_postfix(PadInputState{}); } diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 3ff968d17..b9914eff7 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -115,9 +115,11 @@ APS5_PAD_INPUT_FILE=/tmp/anyps5-pad ./app.elf python3 tools/pad_input.py /tmp/anyps5-pad cross python3 tools/pad_input.py /tmp/anyps5-pad down --hold-ms 500 python3 tools/pad_input.py /tmp/anyps5-pad release +python3 tools/pad_input.py /tmp/anyps5-pad release --left-stick 128 0 --hold-ms 2000 +python3 tools/pad_input.py /tmp/anyps5-pad cross --left-stick 255 128 --right-stick 0 128 ``` -Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads and add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. +Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. ### System fonts diff --git a/tools/pad_input.py b/tools/pad_input.py index 8e1bed63d..e68f3185b 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -19,19 +19,27 @@ def main(): parser.add_argument("path", type=Path) parser.add_argument("button", choices=BUTTONS) parser.add_argument("--hold-ms", type=int, default=300) + parser.add_argument("--left-stick", type=int, nargs=2, metavar=("X", "Y")) + parser.add_argument("--right-stick", type=int, nargs=2, metavar=("X", "Y")) args = parser.parse_args() if not 1 <= args.hold_ms <= 5000: parser.error("--hold-ms must be between 1 and 5000") + sticks = "" + if args.left_stick is not None or args.right_stick is not None: + axes = (args.left_stick or [128, 128]) + (args.right_stick or [128, 128]) + if not all(0 <= axis <= 255 for axis in axes): + parser.error("stick axes must be between 0 and 255") + sticks = "".join(f" {axis}" for axis in axes) temporary = None try: with tempfile.NamedTemporaryFile(mode="w", dir=args.path.parent, delete=False) as output: temporary = Path(output.name) - output.write(f"{time.monotonic_ns()} {BUTTONS[args.button]} {args.hold_ms}\n") + output.write(f"{time.monotonic_ns()} {BUTTONS[args.button]} {args.hold_ms}{sticks}\n") os.replace(temporary, args.path) finally: if temporary is not None: temporary.unlink(missing_ok=True) - print(f"Queued {args.button} for {args.hold_ms} ms in {args.path}") + print(f"Queued {args.button}{' with sticks' + sticks if sticks else ''} for {args.hold_ms} ms in {args.path}") if __name__ == "__main__": From c763a78b6778ffcb83b5ea76d43ef634c1826103 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:29:23 +0000 Subject: [PATCH 206/318] feat(relinker): split unaligned 256-bit stores for Rosetta Rosetta 2 ends the process when an unaligned 256-bit store crosses into a page the store may not write ("rosetta error: unexpectedly need to EmulateForward on a synchronous exception") instead of raising the fault. A store inside such a page, and a 128-bit store across one, fault normally. The macOS write watch keeps watched pages read-only between collections, so a guest that stores 32 bytes across the start of a watched page ends the process. --to-rosetta now lowers VMOVUPS, VMOVUPD and VMOVDQU with a 256-bit memory destination to a stub that stores the low half with the same instruction at VEX.L=0 and the high half with VEXTRACTF128 at the operand plus 16, so each half faults like any other store. RIP-relative operands are relocated from the instruction's place in the site. A store whose upper half has no 32-bit displacement fails the conversion. Aligned stores (VMOVAPS, VMOVAPD, VMOVDQA and the non-temporal forms) cannot cross a page and are left alone. Every store is lowered. A 4-byte store has no room for the jump to its stub, so its stub also takes one neighbouring instruction. The following one: a return ends the stub, a call pushes its original return address and jumps, a jump or conditional branch is re-encoded with a 32-bit displacement, and another 256-bit store is lowered in the same stub. Otherwise the preceding one, unless it is a call, a jump, a return or a trap. Register calls, LOOP, JRCXZ and branches with a prefix that changes them do not move. When neither neighbour can move, the store becomes a short jump to an island: a run of movable instructions within reach is moved to its own stub, and the bytes it frees hold the jump to the store's stub. A store with neither fails the conversion. The converter tests compare the halves with clang's encodings for base, SIB, disp8 overflowing to disp32, disp32, RSP-relative, FS-absolute, r13-based and RIP-relative operands, and check that aligned stores, loads, register forms and masked stores are not split. They check the stub of a short store followed by a return, a call (direct, with an address-size prefix, and through a RIP-relative pointer), short, near and RIP-relative jumps, short and near conditional branches and another store; that a following register call, LOOP, JRCXZ, prefixed branch or CLWB makes the store take the instruction before it; islands past instructions already moved, a store that needs its own stub, a branch target, a register call and the short-jump reach; and that a store with no room fails. PPSA12544: 55295 stores split in the executable, 278 of them in the stub of the store before them; every module converts with none kept. AI-assisted: yes --- core/relinker/CMakeLists.txt | 6 + .../x86/Amd64OnlySubstitutionTable.hpp | 1 + .../x86/Amd64OnlySubstitutionTypes.hpp | 1 + .../x86/IAmd64OnlyInstructionMatcher.hpp | 6 + .../include/codegen/x86/StubBodyBuilder.hpp | 10 +- .../include/codegen/x86/WideStoreLowering.hpp | 16 ++ .../include/codegen/x86/WideStoreOperands.hpp | 26 +++ .../codegen/src/Amd64OnlyConverter.cpp | 199 ++++++++++++++++ .../src/x86/Amd64OnlyInstructionMatcher.cpp | 90 ++++--- .../codegen/src/x86/StubBodyBuilder.cpp | 76 +++++- .../codegen/src/x86/WideStoreLowering.cpp | 21 ++ .../codegen/src/x86/WideStoreOperands.cpp | 133 +++++++++++ .../codegen/tests/Amd64OnlyConverterTests.cpp | 220 +++++++++++++++++- core/relinker/main.cpp | 3 +- 14 files changed, 770 insertions(+), 38 deletions(-) create mode 100644 core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp create mode 100644 core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp create mode 100644 core/relinker/codegen/src/x86/WideStoreLowering.cpp create mode 100644 core/relinker/codegen/src/x86/WideStoreOperands.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4e8e1a4ad..ac1ee47ee 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -53,6 +53,8 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp @@ -141,6 +143,8 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ) target_include_directories(amd64_only_windows_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libc/src/specifics/windows) if(BUILD_TESTING) @@ -304,6 +308,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SegmentFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/EntryStubBuilder.cpp diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index 31118179e..7f09e114a 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -41,6 +41,7 @@ inline constexpr Entry kMcommit = {"MCOMMIT", nullptr, 0}; inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0}; inline constexpr Entry kRdpid = {"RDPID", nullptr, 0}; inline constexpr Entry kClwb = {"CLWB", nullptr, 0}; +inline constexpr Entry kWideStore = {"256-bit store", nullptr, 0}; inline constexpr Entry kExtrq = {"EXTRQ", nullptr, 0}; inline constexpr Entry kInsertq = {"INSERTQ", nullptr, 0}; inline constexpr Entry kExtrqRegisterForm = {"EXTRQ register form", nullptr, 0}; diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp index bcb5df816..e2f594642 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp @@ -35,6 +35,7 @@ struct Amd64OnlyMatch { std::size_t ReturnBranchOffset; bool Optional = false; std::vector Relocations = {}; + bool RelocatableWindow = false; }; struct Amd64OnlySubstitutionReport { diff --git a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp index df17eb175..9986e7928 100644 --- a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp +++ b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp @@ -2,6 +2,7 @@ #define CODEGEN_X86_IAMD64ONLYINSTRUCTIONMATCHER_HPP #include +#include #include #include #include @@ -23,6 +24,11 @@ public: std::span> instructions, std::span trailing ) const = 0; + + virtual void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const = 0; }; std::unique_ptr MakeAmd64OnlyInstructionMatcher(Amd64OnlyTarget target = Amd64OnlyTarget::Intel); diff --git a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp index 212eeea54..f2ff3c365 100644 --- a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp +++ b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp @@ -1,6 +1,7 @@ #ifndef CODEGEN_X86_STUBBODYBUILDER_HPP #define CODEGEN_X86_STUBBODYBUILDER_HPP +#include #include #include #include @@ -53,6 +54,10 @@ public: void Restore(std::uint8_t reg); void Raw(std::span bytes); void Move(std::span instruction, std::optional ripDisplacementOffset); + void RipRelative(std::span instruction, std::size_t displacementOffset, std::int64_t target); + void MoveControlFlow(std::span instruction, const DecodedInstructionInfo& info); + void JumpTo(std::int64_t siteTarget); + void Seek(std::int64_t siteOffset); void Advance(std::size_t length); [[nodiscard]] LoweredBody Finish(); @@ -63,12 +68,15 @@ private: std::size_t ConstantIndex; }; + void _rel32(std::initializer_list opcode, std::int64_t siteTarget); + void _pushReturnAddress(std::int64_t siteTarget); + std::vector _bytes; std::vector _fixups; std::vector _constants; std::vector _relocations; std::size_t _stackDepth = 0; - std::size_t _siteOffset = 0; + std::int64_t _siteOffset = 0; }; } diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp new file mode 100644 index 000000000..7e27e2796 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp @@ -0,0 +1,16 @@ +#ifndef CODEGEN_X86_WIDESTORELOWERING_HPP +#define CODEGEN_X86_WIDESTORELOWERING_HPP + +#include +#include + +namespace Codegen { + +class WideStoreLowering { +public: + void EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const; +}; + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp new file mode 100644 index 000000000..1716af37b --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp @@ -0,0 +1,26 @@ +#ifndef CODEGEN_X86_WIDESTOREOPERANDS_HPP +#define CODEGEN_X86_WIDESTOREOPERANDS_HPP + +#include +#include +#include +#include + +namespace Codegen { + +struct WideStoreHalf { + std::vector Bytes; + std::optional RipDisplacementOffset; + std::int64_t RipTarget = 0; +}; + +struct WideStoreOperands { + WideStoreHalf Low; + WideStoreHalf High; +}; + +[[nodiscard]] std::optional DecodeVexWideStore(const std::uint8_t* data, std::size_t length); + +} + +#endif diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index 201630464..2f57bd9fd 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -3,9 +3,13 @@ #include #include #include +#include #include +#include #include +#include #include +#include #include #include #include @@ -24,6 +28,181 @@ auto _atFileOffset(const Domain::FileByteOffset base, const TOperation& operatio } } +constexpr std::int64_t kShortJumpSize = 2; +constexpr std::uint64_t kIslandSearchBytes = 0x100; + +struct Placement { + std::vector Sites; + std::vector Claimed; + std::vector Consumed; + std::size_t ReplacementLength = 0; +}; + +class WindowPlacer { +public: + WindowPlacer( + std::vector& seg, + const std::vector& matches, + const Domain::ProgramHeader& ph, + const std::set& branchTargets, + const std::set& pending, + const std::set& claimed, + const IAmd64OnlyInstructionMatcher& matcher + ) : _seg(seg), _matches(matches), _ph(ph), _branchTargets(branchTargets), _pending(pending), _claimed(claimed), _matcher(matcher) {} + + [[nodiscard]] std::optional Window(const std::size_t index) const { + if (index + 1 < _matches.size() && (_pending.contains(index + 1) || _movable(index + 1, true))) { + if (auto placement = _relocate(index, index, index + 1)) + return placement; + } + if (index > 0 && _movable(index - 1, false)) + return _relocate(index, index - 1, index); + return std::nullopt; + } + + [[nodiscard]] std::optional Island(const std::size_t index) { + const auto store = _address(index); + for (std::size_t distance = 1;; ++distance) { + bool inReach = false; + for (const bool before : {true, false}) { + if (before ? distance > index : index + distance >= _matches.size()) + continue; + const auto start = before ? index - distance : index + distance; + if (_address(start) + kIslandSearchBytes < store || _address(start) > store + kIslandSearchBytes) + continue; + inReach = true; + if (auto placement = _islandAt(index, start)) + return placement; + } + if (!inReach) + return std::nullopt; + } + } + +private: + std::vector& _seg; + const std::vector& _matches; + const Domain::ProgramHeader& _ph; + const std::set& _branchTargets; + const std::set& _pending; + const std::set& _claimed; + const IAmd64OnlyInstructionMatcher& _matcher; + X64InstructionDecoder _decoder; + + [[nodiscard]] std::uint64_t _address(const std::size_t index) const { return _ph.MappedAddress + _matches[index].Offset; } + [[nodiscard]] std::uint64_t _end(const std::size_t index) const { return _address(index) + _matches[index].Length; } + [[nodiscard]] std::span _bytes(const std::size_t index) const { return {_seg.data() + _matches[index].Offset, _matches[index].Length}; } + [[nodiscard]] DecodedInstructionInfo _info(const std::size_t index) const { return _decoder.DecodeInstruction(_seg.data() + _matches[index].Offset, _matches[index].Length); } + + [[nodiscard]] bool _entered(const std::uint64_t start, const std::uint64_t end) const { + const auto hit = _branchTargets.upper_bound(start); + return hit != _branchTargets.end() && *hit < end; + } + + [[nodiscard]] bool _blocksFollowing(const std::size_t index) const { + const auto kind = _info(index).FlowKind; + return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch; + } + + [[nodiscard]] bool _movable(const std::size_t index, const bool last) const { + return !_claimed.contains(index) && !_pending.contains(index) && (last || !_blocksFollowing(index)); + } + + [[nodiscard]] TrampolineSite _site(const std::uint64_t start, const std::size_t length, LoweredBody body) const { + const auto offset = static_cast(start - _ph.MappedAddress); + return { + static_cast(_ph.Offset + offset), + static_cast(start), + length, + std::vector(_seg.begin() + static_cast(offset), _seg.begin() + static_cast(offset + length)), + std::move(body.Bytes), + body.ReturnBranchOffset, + std::move(body.Relocations) + }; + } + + [[nodiscard]] std::optional _relocate(const std::size_t index, const std::size_t first, const std::size_t last) const { + if (_entered(_address(first), _end(last))) + return std::nullopt; + StubBodyBuilder body; + Placement placement; + try { + for (auto k = first; k <= last; ++k) { + if (_pending.contains(k)) { + _matcher.EmitOutOfLine(body, _bytes(k)); + if (k != index) + placement.Consumed.push_back(k); + } else { + body.MoveControlFlow(_bytes(k), _info(k)); + } + placement.Claimed.push_back(k); + } + } catch (const CodegenException&) { + return std::nullopt; + } + placement.Sites.push_back(_site(_address(first), static_cast(_end(last) - _address(first)), body.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + return placement; + } + + [[nodiscard]] std::optional _islandAt(const std::size_t index, const std::size_t start) { + using Amd64OnlySubstitutionTable::kJmpRel32; + std::size_t head = 0; + for (auto last = start; last < _matches.size(); ++last) { + if (!_movable(last, true) || _entered(_address(start), _end(last))) + return std::nullopt; + if (head == 0 && _end(last) - _address(start) >= kJmpRel32.Size) + head = static_cast(_end(last) - _address(start)); + const auto island = _address(start) + head; + const auto reach = static_cast(island) - static_cast(_address(index) + kShortJumpSize); + if (head != 0 && (reach < std::numeric_limits::min() || reach > std::numeric_limits::max())) + return std::nullopt; + if (head != 0 && _end(last) >= island + kJmpRel32.Size) { + try { + return _buildIsland(index, start, last, head); + } catch (const CodegenException&) { + return std::nullopt; + } + } + if (_blocksFollowing(last)) + return std::nullopt; + } + return std::nullopt; + } + + [[nodiscard]] Placement _buildIsland(const std::size_t index, const std::size_t start, const std::size_t last, const std::size_t head) { + using Amd64OnlySubstitutionTable::kJmpRel32; + const auto windowStart = _address(start); + const auto windowEnd = _end(last); + const auto island = windowStart + head; + const auto store = _address(index); + Placement placement; + + StubBodyBuilder moved; + for (auto k = start; k <= last; ++k) { + moved.MoveControlFlow(_bytes(k), _info(k)); + placement.Claimed.push_back(k); + } + if (!_blocksFollowing(last)) + moved.JumpTo(static_cast(windowEnd - windowStart)); + placement.Sites.push_back(_site(windowStart, head, moved.Finish())); + + StubBodyBuilder lowered; + lowered.Seek(static_cast(store) - static_cast(island)); + _matcher.EmitOutOfLine(lowered, _bytes(index)); + lowered.JumpTo(static_cast(_end(index)) - static_cast(island)); + placement.Sites.push_back(_site(island, kJmpRel32.Size, lowered.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + placement.Claimed.push_back(index); + + const auto offset = _matches[index].Offset; + std::fill_n(_seg.begin() + static_cast(offset), _matches[index].Length, Amd64OnlySubstitutionTable::kTrapFill); + _seg[offset] = X64OpcodeConstants::OneByteJmpRel8; + _seg[offset + 1] = static_cast(static_cast(static_cast(island) - static_cast(store + kShortJumpSize))); + return placement; + } +}; + class Amd64OnlyConverter : public IAmd64OnlyConverter { public: explicit Amd64OnlyConverter(const Amd64OnlyTarget target) : _matcher(MakeAmd64OnlyInstructionMatcher(target)) {} @@ -106,6 +285,10 @@ void Amd64OnlyConverter::_convertSegment( }); const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph) : std::set{}; + std::set pendingIndices; + for (const auto& item : pending) + pendingIndices.insert(item.Index); + std::set claimed; std::set consumed; for (const auto& item : pending) { if (consumed.contains(item.Index)) @@ -126,6 +309,20 @@ void Amd64OnlyConverter::_convertSegment( break; } case Amd64OnlyLowering::Trampoline: { + if (substitution.RelocatableWindow && match.Length < Amd64OnlySubstitutionTable::kJmpRel32.Size) { + WindowPlacer placer(seg, matches, ph, branchTargets, pendingIndices, claimed, *_matcher); + auto placement = placer.Window(item.Index); + if (!placement) + placement = placer.Island(item.Index); + if (!placement) + throw CodegenException("No room for a jump to the stub of " + substitution.InstructionName + " within its neighbours or a nearby island", fileOffset); + for (auto& site : placement->Sites) + result.Trampolines.push_back(std::move(site)); + claimed.insert(placement->Claimed.begin(), placement->Claimed.end()); + consumed.insert(placement->Consumed.begin(), placement->Consumed.end()); + replacementLength = placement->ReplacementLength; + break; + } std::vector taken; try { std::size_t siteLength = match.Length; @@ -172,6 +369,8 @@ void Amd64OnlyConverter::_convertSegment( }); replacementLength = stub.StubBody.size(); consumed.insert(taken.begin(), taken.end()); + for (auto k = item.Index; k < matches.size() && matches[k].Offset < match.Offset + siteLength; ++k) + claimed.insert(k); } catch (const CodegenException&) { if (!substitution.Optional) throw; diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 5d33e8f82..edf59c514 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -72,8 +74,8 @@ const Entry& _sha1Entry(const Sha1Operands& operands) { return kSha1rnds4; } -Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false) { - return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations)}; +Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false, const bool window = false) { + return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations), window}; } void _moveTrailing(StubBodyBuilder& body, std::span trailing) { @@ -142,6 +144,11 @@ public: std::span trailing ) const override; + void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const override; + private: Amd64OnlyTarget _target; Sse4aLowering _lowering; @@ -149,6 +156,7 @@ private: Sha1Lowering _sha1Lowering; ClzeroLowering _clzeroLowering; ReciprocalLowering _reciprocalLowering; + WideStoreLowering _wideStoreLowering; [[nodiscard]] Amd64OnlyMatch _matchMovnts(const DecodedInstruction& instr, const Entry& entry) const; [[nodiscard]] Amd64OnlyMatch _matchSse4a(const DecodedInstruction& instr, const Entry& entry, const Entry& registerFormEntry, std::span trailing) const; @@ -156,6 +164,7 @@ private: [[nodiscard]] Amd64OnlyMatch _matchSha1(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchClzero(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] std::optional _matchRosetta(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] const char* _emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { @@ -208,9 +217,52 @@ std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const D return _inPlace(kClwb, instr.Length, {}); if (instr.IsRdpid()) return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true); + if (const auto wide = DecodeVexWideStore(instr.Data, instr.Length)) + return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true); return std::nullopt; } +const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const { + if (instr.IsExtrq() || instr.IsInsertq()) { + const auto operands = DecodeSse4a(instr.Data, instr.Length); + _lowering.EmitOutOfLine(body, operands); + return _sse4aEntry(operands).Name; + } + if (instr.IsSha256()) { + const auto operands = DecodeSha256(instr.Data, instr.Length); + _sha256Lowering.EmitOutOfLine(body, operands); + return _sha256Entry(operands).Name; + } + if (instr.IsSha1()) { + const auto operands = DecodeSha1(instr.Data, instr.Length); + _sha1Lowering.EmitOutOfLine(body, operands); + return _sha1Entry(operands).Name; + } + if (instr.IsClzero()) { + _clzeroLowering.EmitOutOfLine(body, DecodeClzero(instr.Data, instr.Length)); + return kClzero.Name; + } + if (const auto reciprocal = DecodeReciprocal(instr.Data, instr.Length)) { + _reciprocalLowering.EmitOutOfLine(body, *reciprocal); + return _reciprocalEntry(*reciprocal).Name; + } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { + body.Raw(_rdpidReplacement(instr)); + return kRdpid.Name; + } + if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { + _wideStoreLowering.EmitOutOfLine(body, *wide); + return kWideStore.Name; + } + return nullptr; +} + +void Amd64OnlyInstructionMatcher::EmitOutOfLine(StubBodyBuilder& body, const std::span instruction) const { + if (_emitOutOfLine(body, DecodedInstruction{instruction.data(), instruction.size()}) == nullptr) + throw CodegenException("AMD-only instruction has no out-of-line lowering"); + body.Advance(instruction.size()); +} + std::optional Amd64OnlyInstructionMatcher::MatchSequence( std::span> instructions, std::span trailing @@ -221,37 +273,11 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( const char* name = nullptr; for (const auto& instruction : instructions) { const DecodedInstruction instr{instruction.data(), instruction.size()}; - if (instr.IsExtrq() || instr.IsInsertq()) { - const auto operands = DecodeSse4a(instr.Data, instr.Length); - if (name == nullptr) - name = _sse4aEntry(operands).Name; - _lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha256()) { - const auto operands = DecodeSha256(instr.Data, instr.Length); - if (name == nullptr) - name = _sha256Entry(operands).Name; - _sha256Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha1()) { - const auto operands = DecodeSha1(instr.Data, instr.Length); - if (name == nullptr) - name = _sha1Entry(operands).Name; - _sha1Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsClzero()) { - const auto operands = DecodeClzero(instr.Data, instr.Length); - if (name == nullptr) - name = kClzero.Name; - _clzeroLowering.EmitOutOfLine(body, operands); - } else if (const auto reciprocal = DecodeReciprocal(instr.Data, instr.Length)) { - if (name == nullptr) - name = _reciprocalEntry(*reciprocal).Name; - _reciprocalLowering.EmitOutOfLine(body, *reciprocal); - } else if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { - if (name == nullptr) - name = kRdpid.Name; - body.Raw(_rdpidReplacement(instr)); - } else { + const auto emitted = _emitOutOfLine(body, instr); + if (emitted == nullptr) return std::nullopt; - } + if (name == nullptr) + name = emitted; body.Advance(instr.Length); } _moveTrailing(body, trailing); diff --git a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp index a1b6e44b6..e68805215 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace Codegen { @@ -150,7 +151,7 @@ void StubBodyBuilder::Load(const std::uint8_t reg, const MemoryOperand& operand) if (operand.Rm == ModRmRmSibPresent) _bytes.push_back(operand.Sib); if (operand.RipRelative) { - _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, static_cast(_siteOffset + operand.InstructionLength) + operand.Displacement}); + _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, _siteOffset + static_cast(operand.InstructionLength) + operand.Displacement}); _bytes.insert(_bytes.end(), Disp32Size, 0); return; } @@ -189,13 +190,82 @@ void StubBodyBuilder::Move(const std::span instruction, cons value |= static_cast(instruction[*ripDisplacementOffset + index]) << (index * 8); _bytes[start + *ripDisplacementOffset + index] = 0; } - _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), static_cast(_siteOffset + instruction.size()) + static_cast(value)}); + _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), _siteOffset + static_cast(instruction.size()) + static_cast(value)}); } Advance(instruction.size()); } +void StubBodyBuilder::RipRelative(const std::span instruction, const std::size_t displacementOffset, const std::int64_t target) { + using namespace X64OpcodeConstants; + if (displacementOffset > instruction.size() || instruction.size() - displacementOffset < Disp32Size) + throw CodegenException("RIP-relative displacement lies outside its instruction"); + const auto start = _bytes.size(); + Raw(instruction); + std::fill_n(_bytes.begin() + static_cast(start + displacementOffset), Disp32Size, std::uint8_t{0}); + _relocations.push_back({start + displacementOffset, start + instruction.size(), _siteOffset + target}); +} + +void StubBodyBuilder::MoveControlFlow(const std::span instruction, const DecodedInstructionInfo& info) { + using namespace X64OpcodeConstants; + const auto end = _siteOffset + static_cast(instruction.size()); + if (info.FlowKind == ControlFlowKind::IndirectCall) + throw CodegenException("A register or memory call cannot move into a stub"); + if (info.FlowKind == ControlFlowKind::Call && info.HasRipRelativeDisp) { + _pushReturnAddress(end); + std::vector jump(instruction.begin(), instruction.end()); + auto& modrm = jump[info.RipRelativeDispOffset - 1]; + modrm = static_cast((modrm & ~(ModRmRegMask << ModRmRegShift)) | (4u << ModRmRegShift)); + Move(jump, info.RipRelativeDispOffset); + return; + } + if (!info.HasBranchTarget || info.HasRipRelativeDisp) { + Move(instruction, info.HasRipRelativeDisp ? std::optional(info.RipRelativeDispOffset) : std::nullopt); + return; + } + for (std::size_t index = 0; index < info.OpcodeOffset; ++index) { + const auto prefix = instruction[index]; + if (prefix != PrefixSegCs && prefix != PrefixSegDs && prefix != PrefixRepne && prefix != PrefixAddressSize && (prefix & 0xF0) != kRexBase) + throw CodegenException("A relative branch with this prefix cannot move into a stub"); + } + const auto target = end + info.BranchDisp; + if (info.IsTwoByteOpcode) { + _rel32({TwoByteOpcodeEscape, info.Opcode}, target); + } else if (info.Opcode == OneByteCallRel32) { + _pushReturnAddress(end); + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode == OneByteJmpRel32 || info.Opcode == OneByteJmpRel8) { + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode >= OneByteJccRel8Min && info.Opcode <= OneByteJccRel8Max) { + _rel32({TwoByteOpcodeEscape, static_cast(TwoByteJccRel32Min | (info.Opcode & 0x0F))}, target); + } else { + throw CodegenException("A LOOP or JRCXZ branch cannot move into a stub"); + } + Advance(instruction.size()); +} + +void StubBodyBuilder::JumpTo(const std::int64_t siteTarget) { + _rel32({X64OpcodeConstants::OneByteJmpRel32}, siteTarget); +} + +void StubBodyBuilder::Seek(const std::int64_t siteOffset) { + _siteOffset = siteOffset; +} + void StubBodyBuilder::Advance(const std::size_t length) { - _siteOffset += length; + _siteOffset += static_cast(length); +} + +void StubBodyBuilder::_rel32(const std::initializer_list opcode, const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), opcode); + const auto displacement = _bytes.size(); + _bytes.insert(_bytes.end(), X64OpcodeConstants::Disp32Size, std::uint8_t{0}); + _relocations.push_back({displacement, _bytes.size(), siteTarget}); +} + +void StubBodyBuilder::_pushReturnAddress(const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50}); + _rel32({0x48, 0x8D, 0x05}, siteTarget); + _bytes.insert(_bytes.end(), {0x48, 0x89, 0x44, 0x24, 0x08, 0x58}); } LoweredBody StubBodyBuilder::Finish() { diff --git a/core/relinker/codegen/src/x86/WideStoreLowering.cpp b/core/relinker/codegen/src/x86/WideStoreLowering.cpp new file mode 100644 index 000000000..40e22c9a3 --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreLowering.cpp @@ -0,0 +1,21 @@ +#include + +namespace Codegen { + +namespace { + +void _emitHalf(StubBodyBuilder& body, const WideStoreHalf& half) { + if (half.RipDisplacementOffset) + body.RipRelative(half.Bytes, *half.RipDisplacementOffset, half.RipTarget); + else + body.Raw(half.Bytes); +} + +} + +void WideStoreLowering::EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const { + _emitHalf(body, operands.Low); + _emitHalf(body, operands.High); +} + +} diff --git a/core/relinker/codegen/src/x86/WideStoreOperands.cpp b/core/relinker/codegen/src/x86/WideStoreOperands.cpp new file mode 100644 index 000000000..478c4108e --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreOperands.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::uint8_t kVexMap0F = 0x01; +constexpr std::uint8_t kVexLength256 = 0x04; +constexpr std::uint8_t kVexUnusedRegister = 0x0F; +constexpr std::uint8_t kPpNone = 0x00; +constexpr std::uint8_t kPp66 = 0x01; +constexpr std::uint8_t kPpF3 = 0x02; +constexpr std::uint8_t kVextractf128 = 0x19; +constexpr std::uint8_t kVextractf128Payload = 0x7D; +constexpr std::uint8_t kUpperHalf = 0x01; +constexpr std::int64_t kHalfBytes = 16; + +bool _unalignedStore(const std::uint8_t pp, const std::uint8_t opcode) { + return ((pp == kPpNone || pp == kPp66) && opcode == 0x11) || (pp == kPpF3 && opcode == 0x7F); +} + +bool _legacyPrefixAllowed(const std::uint8_t prefix) { + using namespace X64OpcodeConstants; + return prefix == PrefixSegCs || prefix == PrefixSegSs || prefix == PrefixSegDs || prefix == PrefixSegEs || prefix == PrefixSegFs || prefix == PrefixSegGs || prefix == PrefixAddressSize; +} + +void _appendU32(std::vector& bytes, const std::uint32_t value) { + for (std::size_t index = 0; index < X64OpcodeConstants::Disp32Size; ++index) + bytes.push_back(static_cast(value >> (index * 8))); +} + +} + +std::optional DecodeVexWideStore(const std::uint8_t* data, const std::size_t length) { + using namespace X64OpcodeConstants; + std::size_t pos = 0; + while (pos < length && _legacyPrefixAllowed(data[pos])) + ++pos; + const auto vex = pos; + if (vex + 2 > length) + return std::nullopt; + std::uint8_t inverted = 0; + std::uint8_t payload = 0; + if (data[vex] == OneByteVex2) { + inverted = static_cast((data[vex + 1] & 0x80) | 0x60); + payload = data[vex + 1]; + pos = vex + Vex2PrefixLength; + } else if (data[vex] == OneByteVex3) { + if (vex + Vex3PrefixLength > length || (data[vex + 1] & Vex3MapMask) != kVexMap0F) + return std::nullopt; + inverted = static_cast(data[vex + 1] & 0xE0); + payload = data[vex + 2]; + pos = vex + Vex3PrefixLength; + } else { + return std::nullopt; + } + if ((payload & kVexLength256) == 0 || ((payload >> 3) & 0x0F) != kVexUnusedRegister) + return std::nullopt; + if (pos + 2 > length || !_unalignedStore(static_cast(payload & 0x03), data[pos])) + return std::nullopt; + const auto modRmOffset = pos + 1; + const auto modrm = data[modRmOffset]; + const auto mod = static_cast((modrm >> ModRmModShift) & ModRmModMask); + const auto rm = static_cast(modrm & ModRmRmMask); + if (mod == ModRmModRegister) + return std::nullopt; + auto cursor = modRmOffset + 1; + std::optional sib; + if (rm == ModRmRmSibPresent) { + if (cursor >= length) + return std::nullopt; + sib = data[cursor++]; + } + const bool ripRelative = mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const bool absolute = mod == ModRmModIndirect && sib.has_value() && (*sib & SibBaseMask) == SibBaseDisp32; + const std::size_t displacementSize = mod == ModRmModDisp8 ? Disp8Size : (mod == ModRmModDisp32 || ripRelative || absolute) ? Disp32Size : 0; + if (cursor + displacementSize != length) + return std::nullopt; + std::int64_t displacement = 0; + if (displacementSize == Disp8Size) { + displacement = static_cast(data[cursor]); + } else if (displacementSize == Disp32Size) { + std::uint32_t value = 0; + for (std::size_t index = 0; index < Disp32Size; ++index) + value |= static_cast(data[cursor + index]) << (index * 8); + displacement = static_cast(value); + } + + WideStoreOperands operands; + operands.Low.Bytes.assign(data, data + length); + operands.Low.Bytes[data[vex] == OneByteVex2 ? vex + 1 : vex + 2] &= static_cast(~kVexLength256); + if (ripRelative) { + operands.Low.RipDisplacementOffset = cursor; + operands.Low.RipTarget = static_cast(length) + displacement; + } + + auto& high = operands.High.Bytes; + high.assign(data, data + vex); + high.insert(high.end(), {OneByteVex3, static_cast(inverted | Vex3Map0F3A), kVextractf128Payload, kVextractf128}); + const auto reg = static_cast(modrm & (ModRmRegMask << ModRmRegShift)); + std::int64_t highDisplacement = displacement + kHalfBytes; + std::uint8_t highMod = mod; + std::size_t highDisplacementSize = displacementSize; + if (!ripRelative && !absolute) { + if (mod == ModRmModIndirect || (mod == ModRmModDisp8 && highDisplacement > std::numeric_limits::max())) { + highMod = highDisplacement > std::numeric_limits::max() ? ModRmModDisp32 : ModRmModDisp8; + highDisplacementSize = highMod == ModRmModDisp8 ? Disp8Size : Disp32Size; + } + } + if (!ripRelative && highDisplacement > std::numeric_limits::max()) + throw CodegenException("256-bit store displacement leaves no room for its upper half"); + high.push_back(static_cast((highMod << ModRmModShift) | reg | rm)); + if (sib) + high.push_back(*sib); + const auto highDisplacementOffset = high.size(); + if (highDisplacementSize == Disp8Size) + high.push_back(static_cast(static_cast(highDisplacement))); + else if (ripRelative) + _appendU32(high, 0); + else if (highDisplacementSize == Disp32Size) + _appendU32(high, static_cast(static_cast(highDisplacement))); + high.push_back(kUpperHalf); + if (ripRelative) { + operands.High.RipDisplacementOffset = highDisplacementOffset; + operands.High.RipTarget = static_cast(length) + displacement + kHalfBytes; + } + return operands; +} + +} diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 719788893..16e891044 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -670,6 +671,221 @@ void rosettaSubstitutions() { require(rdpid->ReturnBranchOffset == 6 && Bytes(rdpid->StubBody.begin(), rdpid->StubBody.begin() + 7) == Bytes{0x41, 0xBB, 0, 0, 0, 0, 0xE9}, "RDPID r11 stub does not load 0 before returning"); } +void wideStoreSplit() { + struct Case { + Bytes Original; + Bytes Low; + Bytes High; + const char* Name; + }; + const std::vector cases = { + {{0xC5, 0xFC, 0x11, 0x07}, {0xC5, 0xF8, 0x11, 0x07}, {0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}, "vmovups (%rdi)"}, + {{0xC4, 0x41, 0x7E, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x41, 0x7A, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x43, 0x7D, 0x19, 0x8C, 0x87, 0x80, 0x00, 0x00, 0x00, 0x01}, "vmovdqu 0x70(%r15,%rax,4)"}, + {{0xC5, 0xFC, 0x11, 0x4C, 0x24, 0xF8}, {0xC5, 0xF8, 0x11, 0x4C, 0x24, 0xF8}, {0xC4, 0xE3, 0x7D, 0x19, 0x4C, 0x24, 0x08, 0x01}, "vmovups -8(%rsp)"}, + {{0xC5, 0xFE, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC5, 0xFA, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC4, 0xE3, 0x7D, 0x19, 0x93, 0x88, 0x56, 0x34, 0x12, 0x01}, "vmovdqu 0x12345678(%rbx)"}, + {{0x64, 0xC5, 0xFC, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC5, 0xF8, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC4, 0xE3, 0x7D, 0x19, 0x04, 0x25, 0x50, 0x00, 0x00, 0x00, 0x01}, "vmovups %fs:0x40"}, + {{0xC4, 0x41, 0x7C, 0x11, 0x65, 0x00}, {0xC4, 0x41, 0x78, 0x11, 0x65, 0x00}, {0xC4, 0x43, 0x7D, 0x19, 0x65, 0x10, 0x01}, "vmovups (%r13)"}, + {{0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}, {0xC4, 0xC1, 0x78, 0x11, 0x47, 0x68}, {0xC4, 0xC3, 0x7D, 0x19, 0x47, 0x78, 0x01}, "vmovups 0x68(%r15)"}}; + for (const auto& item : cases) { + const auto split = Codegen::DecodeVexWideStore(item.Original.data(), item.Original.size()); + require(split && split->Low.Bytes == item.Low && split->High.Bytes == item.High && !split->Low.RipDisplacementOffset && !split->High.RipDisplacementOffset, (std::string("256-bit store was not split into its 128-bit halves: ") + item.Name).c_str()); + } + const Bytes rip = {0xC5, 0xFD, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00}; + const auto ripSplit = Codegen::DecodeVexWideStore(rip.data(), rip.size()); + require(ripSplit && ripSplit->Low.Bytes == Bytes{0xC5, 0xF9, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00} && ripSplit->High.Bytes == Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x01}, "RIP-relative 256-bit store was not split"); + require(ripSplit->Low.RipDisplacementOffset == 4 && ripSplit->Low.RipTarget == 8 + 0x100 && ripSplit->High.RipDisplacementOffset == 5 && ripSplit->High.RipTarget == 8 + 0x110, "RIP-relative halves do not address the original operand and the one 16 bytes above it"); + for (const auto& other : {Bytes{0xC5, 0xF8, 0x11, 0x07}, Bytes{0xC5, 0xFC, 0x11, 0xC1}, Bytes{0xC5, 0xFC, 0x10, 0x07}, Bytes{0xC4, 0xE2, 0x75, 0x2E, 0x07}, Bytes{0xC5, 0xF4, 0x11, 0x07}, Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x07, 0x01}, Bytes{0xC5, 0xFC, 0x11, 0x04}, Bytes{0xC5, 0xFC, 0x11, 0x47}}) + require(!Codegen::DecodeVexWideStore(other.data(), other.size()).has_value(), "An instruction other than a plain 256-bit store was split"); + for (const auto& aligned : {Bytes{0xC5, 0xFC, 0x29, 0x4C, 0x24, 0xF8}, Bytes{0xC5, 0xFD, 0xE7, 0x93, 0x78, 0x56, 0x34, 0x12}, Bytes{0xC5, 0xFD, 0x7F, 0x1F}, Bytes{0xC5, 0xFC, 0x2B, 0x27}}) + require(!Codegen::DecodeVexWideStore(aligned.data(), aligned.size()).has_value(), "An aligned 256-bit store, which cannot cross a page, was split"); + const Bytes farRip = {0xC5, 0xFC, 0x11, 0x05, 0xF8, 0xFF, 0xFF, 0x7F}; + const auto farRipSplit = Codegen::DecodeVexWideStore(farRip.data(), farRip.size()); + require(farRipSplit && farRipSplit->High.RipTarget == std::int64_t{8} + 0x7FFFFFF8 + 0x10, "A RIP-relative 256-bit store near the end of its displacement range was not split"); + const Bytes farBase = {0xC5, 0xFC, 0x11, 0x80, 0xF8, 0xFF, 0xFF, 0x7F}; + requireFailure([&] { (void)Codegen::DecodeVexWideStore(farBase.data(), farBase.size()); }, "A 256-bit store whose upper half has no 32-bit displacement was kept"); + requireFailure([] { Codegen::StubBodyBuilder body; body.RipRelative(Bytes{0x90}, 0, 0); }, "A RIP-relative displacement outside its instruction was relocated"); + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}; + const auto lowered = rosetta->Match(store.data(), store.size()); + require(lowered && lowered->Lowering == Codegen::Amd64OnlyLowering::Trampoline && !lowered->Optional && lowered->RelocatableWindow && lowered->InstructionName == "256-bit store", "A 256-bit store was not lowered through a required stub that may move its neighbours for Rosetta"); + Bytes expectedBody = cases.back().Low; + expectedBody.insert(expectedBody.end(), cases.back().High.begin(), cases.back().High.end()); + require(Bytes(lowered->StubBody.begin(), lowered->StubBody.begin() + static_cast(lowered->ReturnBranchOffset)) == expectedBody, "The 256-bit store stub is not the two halves"); + const std::vector> pair = {std::span(cases[0].Original), std::span(rip)}; + const auto sequence = rosetta->MatchSequence(pair, {}); + require(sequence && sequence->Relocations.size() == 2 && sequence->Relocations[0].SiteTarget == 4 + 8 + 0x100 && sequence->Relocations[1].SiteTarget == 4 + 8 + 0x110, "A RIP-relative 256-bit store after another is not relocated from its place in the site"); + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(store.data(), store.size()).has_value(), "A 256-bit store was split for an Intel host"); + const std::vector> single = {std::span(store)}; + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->MatchSequence(single, {}).has_value(), "A 256-bit store was split in a sequence for an Intel host"); +} + +void converterWideStoreWindows() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC5, 0xFC, 0x11, 0x07}; + Bytes halves = {0xC5, 0xF8, 0x11, 0x07, 0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}; + const auto convert = [&](const Bytes& text) { + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {segmentHeader(text.size())}); + }; + const auto body = [](const Codegen::TrampolineSite& site) { + return Bytes(site.Body.begin(), site.Body.begin() + static_cast(site.ReturnBranchOffset)); + }; + const auto withStore = [&](Bytes before, const Bytes& after) { + before.insert(before.end(), store.begin(), store.end()); + before.insert(before.end(), after.begin(), after.end()); + return before; + }; + + const auto returned = convert(withStore({}, {0xC3})); + Bytes expected = halves; + expected.push_back(0xC3); + require(returned.Trampolines.size() == 1 && returned.KeptCount == 0 && returned.Reports.size() == 1 && returned.Reports[0].InstructionName == "256-bit store", "A 256-bit store followed by a return was not moved"); + require(returned.Trampolines[0].Offset == 0x200 && returned.Trampolines[0].Length == 5 && body(returned.Trampolines[0]) == expected, "A 256-bit store followed by a return did not end its stub with the return"); + + const auto called = convert(withStore({}, {0xE8, 0x10, 0x00, 0x00, 0x00})); + require(called.Trampolines.size() == 1 && called.Trampolines[0].Offset == 0x200 && called.Trampolines[0].Length == 9, "A 256-bit store followed by a call did not take the call"); + const auto& call = called.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58, 0xE9, 0, 0, 0, 0}); + require(body(call) == expected, "A moved call does not push its original return address and jump"); + require(call.Relocations.size() == 2 && call.Relocations[0].DisplacementOffset == halves.size() + 9 && call.Relocations[0].SiteTarget == 9 && call.Relocations[1].DisplacementOffset == halves.size() + 20 && call.Relocations[1].SiteTarget == 9 + 0x10, "A moved call returns to or calls the wrong address"); + + const auto branched = convert(withStore({}, {0x75, 0x10, 0x90})); + require(branched.Trampolines.size() == 1 && branched.Trampolines[0].Length == 6, "A 256-bit store followed by a short conditional branch did not take the branch"); + const auto& jcc = branched.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(body(jcc) == expected && jcc.Relocations.size() == 1 && jcc.Relocations[0].SiteTarget == 6 + 0x10, "A moved short conditional branch was not re-encoded with a 32-bit displacement to its target"); + + const auto jumped = convert(withStore({}, {0xEB, 0x10, 0x90})); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(jumped.Trampolines.size() == 1 && body(jumped.Trampolines[0]) == expected && jumped.Trampolines[0].Relocations.size() == 1 && jumped.Trampolines[0].Relocations[0].SiteTarget == 6 + 0x10, "A moved short jump was not re-encoded with a 32-bit displacement to its target"); + + const auto longBranched = convert(withStore({}, {0x0F, 0x85, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(longBranched.Trampolines.size() == 1 && longBranched.Trampolines[0].Length == 10 && body(longBranched.Trampolines[0]) == expected && longBranched.Trampolines[0].Relocations.size() == 1 && longBranched.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved conditional branch with a 32-bit displacement does not reach its target"); + + const auto entered = convert(withStore({0x48, 0x83, 0xC1, 0x01}, {0xC3, 0xEB, 0xFD})); + expected = {0x48, 0x83, 0xC1, 0x01}; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(entered.Trampolines.size() == 1 && entered.Trampolines[0].Offset == 0x200 && entered.Trampolines[0].Length == 8 && body(entered.Trampolines[0]) == expected, "A 256-bit store before a branch target did not take room from the instruction before it"); + + Bytes adds; + for (int index = 0; index < 4; ++index) + adds.insert(adds.end(), {0x48, 0x83, 0xC1, 0x01}); + Bytes after = {0xC3, 0xEB, 0xFD}; + after.insert(after.end(), adds.begin(), adds.end()); + const auto island = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, after)); + require(island.Trampolines.size() == 2 && island.KeptCount == 0, "A 256-bit store with no movable neighbours was not lowered through an island"); + const auto& moved = island.Trampolines[0]; + const auto& lowered = island.Trampolines[1]; + expected = adds; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(moved.Offset == 0x20C && moved.Length == 8 && body(moved) == expected && moved.Relocations.size() == 1 && moved.Relocations[0].SiteTarget == 16, "The instructions under the island were not moved and continued after it"); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(lowered.Offset == 0x214 && lowered.Length == 5 && body(lowered) == expected && lowered.Relocations.size() == 1 && lowered.Relocations[0].SiteTarget == -11, "The island does not store both halves and return after the original store"); + require(Bytes(island.Bytes.begin() + 0x205, island.Bytes.begin() + 0x209) == Bytes{0xEB, 0x0D, 0xCC, 0xCC}, "The 256-bit store was not replaced by a short jump to its island"); + + requireFailure([&] { (void)convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, {0xC3, 0xEB, 0xFD})); }, "A 256-bit store with no room for a stub jump was kept"); + + const auto paired = convert(withStore({}, {0xC5, 0xFC, 0x11, 0x0E, 0xC3})); + expected = halves; + expected.insert(expected.end(), {0xC5, 0xF8, 0x11, 0x0E, 0xC4, 0xE3, 0x7D, 0x19, 0x4E, 0x10, 0x01}); + require(paired.Trampolines.size() == 1 && paired.Trampolines[0].Length == 8 && body(paired.Trampolines[0]) == expected && paired.Reports.size() == 1, "A 256-bit store followed by another was not lowered with it in one stub"); + + const Bytes push = {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58}; + const auto ripCalled = convert(withStore({}, {0xFF, 0x15, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + const auto& ripCall = ripCalled.Trampolines[0]; + require(ripCalled.Trampolines.size() == 1 && ripCall.Length == 10 && body(ripCall) == expected, "A moved call through a RIP-relative pointer does not push its original return address and jump through the pointer"); + require(ripCall.Relocations.size() == 2 && ripCall.Relocations[0].SiteTarget == 10 && ripCall.Relocations[1].DisplacementOffset == halves.size() + 21 && ripCall.Relocations[1].SiteTarget == 10 + 0x10, "A moved call through a RIP-relative pointer returns to or reads the wrong address"); + + const auto ripJumped = convert(withStore({}, {0xFF, 0x25, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + require(ripJumped.Trampolines.size() == 1 && body(ripJumped.Trampolines[0]) == expected && ripJumped.Trampolines[0].Relocations.size() == 1 && ripJumped.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved jump through a RIP-relative pointer reads the wrong address"); + + const auto relaxed = convert(withStore({}, {0x67, 0xE8, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(relaxed.Trampolines.size() == 1 && relaxed.Trampolines[0].Length == 10 && body(relaxed.Trampolines[0]) == expected && relaxed.Trampolines[0].Relocations.size() == 2 && relaxed.Trampolines[0].Relocations[1].SiteTarget == 10 + 0x10, "A call with an address-size prefix was not moved like a call"); + + const Bytes add = {0x48, 0x83, 0xC1, 0x01}; + for (const auto& follower : {Bytes{0xFF, 0xD0}, Bytes{0xE3, 0x10}, Bytes{0x66, 0x75, 0x10}, Bytes{0x66, 0x0F, 0xAE, 0x37}}) { + const auto preceded = convert(withStore(add, follower)); + expected = add; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(!preceded.Trampolines.empty() && preceded.Trampolines[0].Offset == 0x200 && preceded.Trampolines[0].Length == 8 && body(preceded.Trampolines[0]) == expected, "A 256-bit store moved a following instruction that cannot leave its place instead of the one before it"); + } + + Bytes throwing = {0xC3, 0xEB, 0xFD}; + for (int index = 0; index < 3; ++index) + throwing.insert(throwing.end(), add.begin(), add.end()); + throwing.insert(throwing.end(), {0xFF, 0xD0}); + throwing.insert(throwing.end(), adds.begin(), adds.end()); + const auto farther = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, throwing)); + require(farther.Trampolines.size() == 2 && farther.Trampolines[0].Offset == 0x21A && farther.Trampolines[1].Offset == 0x222, "An island that would move a register call was not passed over for the next one"); + require(Bytes(farther.Bytes.begin() + 0x205, farther.Bytes.begin() + 0x207) == Bytes{0xEB, 0x1B}, "The 256-bit store does not jump to the island after the register call"); + + Bytes reentered = {0xFF, 0xD0, 0xEB, 0xF8}; + reentered.insert(reentered.end(), adds.begin(), adds.end()); + const auto beyondStore = convert(withStore(Bytes(adds.begin(), adds.begin() + 8), reentered)); + require(beyondStore.Trampolines.size() == 2 && beyondStore.Trampolines[0].Offset == 0x210 && beyondStore.Trampolines[1].Offset == 0x218, "An island before a 256-bit store that is a branch target took the store with it"); + require(Bytes(beyondStore.Bytes.begin() + 0x208, beyondStore.Bytes.begin() + 0x20A) == Bytes{0xEB, 0x0E}, "The 256-bit store that is a branch target does not jump to the island after it"); + + const Bytes farCall = {0xE8, 0x40, 0x00, 0x00, 0x00}; + Bytes claimedFirst = {0xFF, 0xD0}; + claimedFirst.insert(claimedFirst.end(), adds.begin(), adds.end()); + const auto afterSequence = convert(withStore({0xC5, 0xF8, 0x53, 0xC1, 0x48, 0x83, 0xC1, 0x01}, claimedFirst)); + require(afterSequence.Trampolines.size() == 3 && afterSequence.Trampolines[0].Offset == 0x200 && afterSequence.Trampolines[0].Length == 8 && afterSequence.Trampolines[1].Offset == 0x20E && afterSequence.Trampolines[2].Offset == 0x216, "A 256-bit store moved an instruction already moved into the stub of the instruction before it"); + + Bytes pendingRun = {0xFF, 0xD0, 0x48, 0x83, 0xC1, 0x01, 0xC5, 0xFC, 0x11, 0x47, 0x10}; + pendingRun.insert(pendingRun.end(), adds.begin(), adds.end()); + const auto pendingIsland = convert(withStore(farCall, pendingRun)); + require(pendingIsland.Trampolines.size() == 3 && pendingIsland.Trampolines[0].Offset == 0x214 && pendingIsland.Trampolines[1].Offset == 0x21C && pendingIsland.Trampolines[2].Offset == 0x20F && pendingIsland.Trampolines[2].Length == 5, "An island moved the call before a 256-bit store or another store that still needs its own stub"); + + Bytes enteredRun = {0xFF, 0xD0}; + for (int index = 0; index < 6; ++index) + enteredRun.insert(enteredRun.end(), add.begin(), add.end()); + enteredRun.insert(enteredRun.end(), {0xEB, 0xEE}); + const auto enteredIsland = convert(withStore(farCall, enteredRun)); + require(enteredIsland.Trampolines.size() == 2 && enteredIsland.Trampolines[0].Offset == 0x213 && enteredIsland.Trampolines[1].Offset == 0x21B, "An island was placed over a branch target"); + + Bytes returning = {0xFF, 0xD0}; + for (int index = 0; index < 3; ++index) + returning.insert(returning.end(), add.begin(), add.end()); + returning.push_back(0xC3); + const auto returnIsland = convert(withStore(farCall, returning)); + expected = Bytes(adds.begin(), adds.begin() + 12); + expected.push_back(0xC3); + require(returnIsland.Trampolines.size() == 2 && returnIsland.Trampolines[0].Offset == 0x20B && body(returnIsland.Trampolines[0]) == expected, "Instructions moved for an island that end in a return jump back after it"); + + const auto afterBranch = convert(withStore({0x75, 0x10}, {0xFF, 0xD0})); + expected = {0x0F, 0x85, 0, 0, 0, 0}; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(afterBranch.Trampolines.size() == 1 && afterBranch.Trampolines[0].Offset == 0x200 && afterBranch.Trampolines[0].Length == 6 && body(afterBranch.Trampolines[0]) == expected && afterBranch.Trampolines[0].Relocations[0].SiteTarget == 2 + 0x10, "A 256-bit store after a conditional branch did not take the branch"); + + Bytes far = adds; + far.resize(8); + for (int index = 0; index < 60; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + far.insert(far.end(), adds.begin(), adds.end()); + for (int index = 0; index < 70; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + Bytes tail; + for (int index = 0; index < 11; ++index) + tail.insert(tail.end(), {0xFF, 0xD0}); + tail.insert(tail.end(), {0x90, 0x90}); + requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); +} + void converterRosetta() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); Bytes file(0x300, 0xCC); @@ -776,7 +992,7 @@ void converterReciprocal() { void converterStrayRex() { const auto converter = Codegen::MakeAmd64OnlyConverter(); const auto convert = [&](const Bytes& text) { - Bytes file(0x300, 0xCC); + Bytes file(0x300 + text.size(), 0xCC); std::copy(text.begin(), text.end(), file.begin() + 0x200); return std::pair{file, converter->Convert(file, {segmentHeader(text.size())})}; }; @@ -1419,6 +1635,8 @@ int main() { converterClzero(); rosettaSubstitutions(); converterRosetta(); + wideStoreSplit(); + converterWideStoreWindows(); converterReciprocal(); converterStrayRex(); rewriterStrayRex(); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 0e747853d..1734ede1b 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -71,10 +71,11 @@ int Run(const int argc, char* argv[]) { sourceBytes = std::move(converted.Bytes); trampolines = std::move(converted.Trampolines); std::map stubsByName; + const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; for (const auto& report : converted.Reports) { if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; - else if (report.InstructionName == "VRSQRTPS" || report.InstructionName == "VRCPPS") + else if (counted(report.InstructionName)) ++stubsByName[report.InstructionName]; else std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; From bff955a3e44f73b4929311669111d4e3d028da2d Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:55:12 -0300 Subject: [PATCH 207/318] fix(agc): match any format when a storage dump shape names none --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 92f24c687..af6d40a31 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2989,8 +2989,8 @@ const StorageDumpSelector& StorageDumpSelection() { StorageDumpSelector parsed; const char* text = std::getenv("APS5_DUMP_STORAGE"); if (text == nullptr) return parsed; - if (std::sscanf(text, "%ux%ux%u:%u", &parsed.width, &parsed.height, &parsed.layers, &parsed.format) >= 3) { - parsed.anyFormat = std::strchr(text, ':') == nullptr; + if (const int fields = std::sscanf(text, "%ux%ux%u:%u", &parsed.width, &parsed.height, &parsed.layers, &parsed.format); fields >= 3) { + parsed.anyFormat = fields < 4; return parsed; } parsed.address = std::strtoull(text, nullptr, 16); From d119945a4519a340707f8477495aa3048bb30a8f Mon Sep 17 00:00:00 2001 From: Feuris Date: Mon, 12 Oct 2026 00:06:30 -0300 Subject: [PATCH 208/318] fix(libc): allocate from realloc(NULL, 0) as malloc(0) does realloc(NULL, 0) returned NULL without allocating anything, while malloc(0) returns a unique pointer. C11 7.22.3.5p3 says realloc with a null pointer behaves like malloc for the specified size, so code that grows an empty buffer from zero elements (p = realloc(p, n * size) with n == 0) and treats NULL as out of memory failed on its first call. FreeBSD 11.4 never returns NULL from realloc for a zero size; je_realloc rounds the size up before choosing between resizing and allocating (contrib/jemalloc/src/jemalloc.c, je_realloc). A null pointer with size 0 now reaches the allocator's realloc, which allocates as malloc(0) does. A non-null pointer with size 0 is still freed and NULL returned, as before. Co-authored-by: milescoviello --- core/libs/prx/libc/src/ApplicationHeap.cpp | 2 +- core/libs/tests/ApplicationHeap.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index f05e94132..e619429ee 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -176,7 +176,7 @@ void ApplicationHeapFree_nid_no_patch(void* pointer) { } void* ApplicationHeapReallocate_nid_no_patch(void* pointer, std::size_t bytes) { - if (bytes == 0) { + if (bytes == 0 && pointer != nullptr) { ApplicationHeapFree_nid_no_patch(pointer); return nullptr; } diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 04adba57c..d7f755740 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -167,6 +167,12 @@ int main(int argc, char** argv) { reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); *__error_nid_postfix() = 0; require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12); + void* empty = ApplicationHeapAllocate_nid_no_patch(0); + require(empty != nullptr); + ApplicationHeapFree_nid_no_patch(empty); + empty = ApplicationHeapReallocate_nid_no_patch(nullptr, 0); + require(empty != nullptr); + ApplicationHeapFree_nid_no_patch(empty); for (std::size_t alignment : {16, 64, 4096}) { auto* aligned = ApplicationHeapAlign_nid_no_patch(alignment, 37); require(reinterpret_cast(aligned) % alignment == 0); From ef3b121e44ad7d7f721c11dc87b1737bdbcba9ea Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:14:59 -0400 Subject: [PATCH 209/318] fix(libSceAmpr): export sceAmprAmmSetPageTablePoolOccupancyNotificationThreshold Resolves NID touqMEt6qXQ, the last import Trails in the Sky 2nd Chapter (PPSA35169) is left missing on libSceAmpr.prx once the libkernel pair in #4214 is recorded. No reference describes the function's contract, so it throws via the project's NotImplemented_nid_no_patch pattern rather than guessing a result that a caller would act on. sceAmprAmmGetUsageStatsData (KqiWXLgCVe0) is deliberately not included: #3814 exports it and #4207 answers it from a cited layout. See issue #4214. --- core/libs/prx/libSceAmpr/Export.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fb835934f..0a2ac597f 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -822,6 +822,11 @@ int APS5_VABI sceAmprAmmGetVirtualAddressRanges(std::uint64_t* start, std::uint6 return 0; } +int APS5_VABI sceAmprAmmSetPageTablePoolOccupancyNotificationThreshold(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAmprAmmSubmitCommandBuffer(void* base, std::uint32_t offset, std::uint32_t priority) { if (priority > 2u) return SCE_KERNEL_ERROR_EINVAL; if (!base) return SCE_KERNEL_ERROR_EPERM; From bc71f9766713c7cf5ad8776adaa39f5cc5b0eb0b Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Mon, 12 Oct 2026 09:03:06 +0530 Subject: [PATCH 210/318] fix(libkernel): return after NotImplemented in Windows shm_unlink --- core/libs/prx/libkernel/File/src/SharedMemory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 8202db42e..c5b133d94 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -473,6 +473,7 @@ extern "C" int APS5_VABI shm_unlink_nid_postfix(const char* path) { #ifdef _WIN32 (void)path; NotImplemented_nid_no_patch("shm_unlink of a named object"); + return 0; #else int validationError = 0; const auto hostName = NamedHostPath(path, validationError); From 2cf929db08905bb3f3a4d019ff7b5ddb7e900d52 Mon Sep 17 00:00:00 2001 From: ducloc080202 Date: Mon, 12 Oct 2026 10:33:45 +0700 Subject: [PATCH 211/318] fix(libkernel): record AmmModifyMtypeProtect and AmmRemapIntoPrt commands --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 25 ++++++++ core/libs/tests/GuestAmpr.cpp | 84 +++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 110 insertions(+) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8e307d014..6910a43eb 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -882,4 +882,29 @@ int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizesForEach(con return _resolveForEach(prefix, paths, count, ids, sizes, results); } +bool _validWriteRange(std::uint64_t address, std::uint64_t size) { + constexpr std::uint64_t pageMask = 0x3FFFu; + return address != 0u && size != 0u && ((address | size) & pageMask) == 0u && address + size > address; +} + +bool _validWriteProtection(std::uint64_t protection) { + return (protection & ~0x3F3ULL) == 0u; +} + +int APS5_VABI sceKernelWriteModifyMtypeProtectCommand(void* dst, std::uint64_t address, std::uint64_t size, std::uint64_t type, std::uint64_t protection, std::uint64_t mask, std::uint64_t* outSize) { + if (!_validWriteRange(address, size) || !_validWriteProtection(protection) || !_validWriteProtection(mask)) return SCE_KERNEL_ERROR_EINVAL; + const Apr::AmmProtectCommand command{{Apr::Opcode::AmmModifyMtypeProtect, sizeof(command)}, address, size, static_cast(type), static_cast(protection), static_cast(mask), 0}; + if (dst) std::memcpy(dst, &command, sizeof(command)); + if (outSize) *outSize = sizeof(command); + return 0; +} + +int APS5_VABI sceKernelWriteRemapIntoPrtCommand(std::uint32_t* dst, std::uint64_t address, std::uint64_t source, std::uint64_t size, int protection, int opcode, std::uint64_t* outSize) { + if (!_validWriteRange(address, size) || !_validWriteRange(source, size) || !_validWriteProtection(static_cast(protection))) return SCE_KERNEL_ERROR_EINVAL; + const Apr::AmmRemapCommand command{{Apr::Opcode::AmmRemapIntoPrt, sizeof(command)}, address, source, size, protection, opcode ? opcode : 1011}; + if (dst) std::memcpy(dst, &command, sizeof(command)); + if (outSize) *outSize = sizeof(command); + return 0; +} + } diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index d2a646ac1..3b366edd4 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -108,6 +108,8 @@ int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI sceAmprAmmCommandBufferMapAsPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t); int APS5_VABI sceAmprAmmCommandBufferAllocatePaForPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::int32_t, std::int32_t); int APS5_VABI sceAmprAmmCommandBufferRemapIntoPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::uint64_t, std::int32_t, std::uint32_t); +int APS5_VABI sceKernelWriteModifyMtypeProtectCommand(void*, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t*); +int APS5_VABI sceKernelWriteRemapIntoPrtCommand(std::uint32_t*, std::uint64_t, std::uint64_t, std::uint64_t, int, int, std::uint64_t*); int APS5_VABI sceAmprAmmCommandBufferUnmapToPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t); std::int64_t APS5_VABI sceAmprAmmMeasureAmmCommandSizeMapAsPrt(std::uint64_t, std::uint64_t); std::int64_t APS5_VABI sceAmprAmmMeasureAmmCommandSizeAllocatePaForPrt(std::uint64_t, std::uint64_t, std::int32_t, std::int32_t); @@ -1122,6 +1124,87 @@ void TestAmmPrt() { Require(sceAmprAmmMeasureAmmCommandSizeAllocatePaForPrt(prt + 8, page, 0, cpuReadWrite) == ammRejected); } +void TestKernelWriteCommands() { + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0); + std::int64_t pool = -1; + Require(sceAmprAmmGiveDirectMemory(0, static_cast(sceKernelGetDirectMemorySize()), 4 * page, page, 1, &pool) == 0); + + const std::uint64_t target = start + 0x400000; + const std::uint64_t source = start + 0x440000; + const std::uint64_t prt = start + 0x480000; + + Recorder setup; + Require(sceAmprAmmCommandBufferMap(&setup.buffer, target, 2 * page, 0, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMap(&setup.buffer, source, page, 0, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMapAsPrt(&setup.buffer, prt, 2 * page) == 0); + SubmitAmm(setup); + At(source) = 0x9C; + + std::uint64_t measured = 0; + Require(sceKernelWriteModifyMtypeProtectCommand(nullptr, target, 2 * page, 5, 0x13, 0x10, &measured) == 0); + Require(measured == sizeof(Apr::AmmProtectCommand)); + std::array written{}; + std::uint64_t reported = 0; + Require(sceKernelWriteModifyMtypeProtectCommand(written.data(), target, 2 * page, 5, 0x13, 0x10, &reported) == 0); + Require(reported == sizeof(Apr::AmmProtectCommand)); + Apr::CommandHeader header; + std::memcpy(&header, written.data(), sizeof(header)); + Require(header.opcode == Apr::Opcode::AmmModifyMtypeProtect && header.bytes == sizeof(Apr::AmmProtectCommand)); + Recorder sibling; + Require(sceAmprAmmCommandBufferModifyMtypeProtect(&sibling.buffer, target, 2 * page, 5, 0x13, 0x10) == 0); + Require(std::memcmp(written.data(), sibling.memory.data(), sizeof(Apr::AmmProtectCommand)) == 0); + Require(sceKernelWriteModifyMtypeProtectCommand(written.data(), target, 2 * page, 5, 0x13, 0x10, nullptr) == 0); + std::uint32_t id = 0; + Require(sceAmprAmmSubmitCommandBuffer3(written.data(), static_cast(reported), 0, &id) == 0); + Require(sceAmprAmmWaitCommandBufferCompletion(id) == 0); + Require(Protection(target) == 0x13 && Protection(target + page) == 0x13); + At(target) = 0x9D; + Require(At(target) == 0x9D); + + std::uint64_t prtMeasured = 0; + Require(sceKernelWriteRemapIntoPrtCommand(nullptr, prt + page, source, page, cpuReadWrite, 0, &prtMeasured) == 0); + Require(prtMeasured == sizeof(Apr::AmmRemapCommand)); + std::array prtWritten{}; + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(prtWritten.data()), prt + page, source, page, cpuReadWrite, 0, &prtMeasured) == 0); + Apr::AmmRemapCommand remap; + std::memcpy(&remap, prtWritten.data(), sizeof(remap)); + Require(remap.header.opcode == Apr::Opcode::AmmRemapIntoPrt && remap.header.bytes == sizeof(remap)); + Require(remap.address == prt + page && remap.source == source && remap.size == page && remap.protection == cpuReadWrite && remap.reserved == 1011); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(prtWritten.data()), prt + page, source, page, cpuReadWrite, 42, &prtMeasured) == 0); + std::memcpy(&remap, prtWritten.data(), sizeof(remap)); + Require(remap.reserved == 42); + Recorder prtSibling; + Require(sceAmprAmmCommandBufferRemapIntoPrt(&prtSibling.buffer, prt + page, source, page, cpuReadWrite, 0) == 0); + std::array prtSubmit{}; + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(prtSubmit.data()), prt + page, source, page, cpuReadWrite, 0, nullptr) == 0); + Require(std::memcmp(prtSubmit.data(), prtSibling.memory.data(), sizeof(Apr::AmmRemapCommand)) == 0); + Require(sceAmprAmmSubmitCommandBuffer3(prtSubmit.data(), static_cast(sizeof(Apr::AmmRemapCommand)), 0, &id) == 0); + Require(sceAmprAmmWaitCommandBufferCompletion(id) == 0); + Require(At(prt + page) == 0x9C); + + std::array untouched{}; + untouched.fill(0xCC); + std::uint64_t sentinel = 0xDDDDDDDDDDDDDDDDULL; + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target + 8, page, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, 0, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), 0, page, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, page, 0, 0x04, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, page, 0, 0x03, 0x04, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), ~std::uint64_t(0), page, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, page, 0, 0x100000000ULL, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(untouched.data()), prt, source + 8, page, cpuReadWrite, 0, &sentinel) == invalidArgument); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(untouched.data()), prt, source, 0, cpuReadWrite, 0, &sentinel) == invalidArgument); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(untouched.data()), prt, source, page, 0x04, 0, &sentinel) == invalidArgument); + Require(sentinel == 0xDDDDDDDDDDDDDDDDULL); + for (const auto byte : untouched) Require(byte == 0xCC); + Require(sceKernelWriteModifyMtypeProtectCommand(nullptr, target, page, 0, 0x03, 0x03, nullptr) == 0); + Require(sceKernelWriteRemapIntoPrtCommand(nullptr, prt, source, page, cpuReadWrite, 0, nullptr) == 0); +} + } int main() { @@ -1152,5 +1235,6 @@ int main() { TestAmm(); TestAmmRemapAndProtect(); TestAmmPrt(); + TestKernelWriteCommands(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 51f08cc9e..07b98364e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -126,6 +126,7 @@ Throughout the project, every function at every stage either **does exactly what - [AMM remap, multimap and protection changes](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws - [sceVideodec2GetAvcPictureInfo](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) - [AMM PRT ranges](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws +- [AMM kernel write commands](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - signatures of `sceKernelWriteModifyMtypeProtectCommand` and `sceKernelWriteRemapIntoPrtCommand` taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), which forward to the real kernel; the measure mode (`dst` null reports the size, `outSize` null skips the report) follows ampr_emu's measure-then-record calls, not confirmed on a title. The range, protection and mask checks mirror the `libSceAmpr` siblings, and the recorded bytes match them exactly so the submitted buffer executes on the existing paths - [APR map begin and end](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale From c9e48e5ae2e0f4ae900ca22ae7cd67b9e53e7d30 Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 03:29:36 -0700 Subject: [PATCH 212/318] feat(agc): support uniform custom MSAA sample locations --- .../Execution/src/VulkanDevice.cpp | 17 +++++++++ .../Graphics/include/Context.hpp | 1 + .../Graphics/include/Pipeline.hpp | 1 + .../Graphics/include/State.hpp | 3 ++ .../Graphics/src/DepthSurface.cpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 ++++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 22 ++++++++++++ .../libSceAgcDriver/Graphics/src/State.cpp | 33 +++++++++++++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 35 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 10 files changed, 117 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 8a1600be3..970ed7f62 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -284,6 +284,7 @@ struct VulkanDevice::State { bool maintenance8 = false; std::uint32_t srgbDecodeFormats = 0; bool storageImageWithoutFormat = false; + VkSampleCountFlags sampleLocationSampleCounts = 0; bool depthClamp = false; bool depthBounds = false; bool depthBiasClamp = false; @@ -1150,6 +1151,21 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_KHR_physical_storage_buffer"); state->depthRangeUnrestricted = hasExtension(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); if (state->depthRangeUnrestricted) deviceExtensions.push_back(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); + if (hasExtension(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME)) { + VkPhysicalDeviceSampleLocationsPropertiesEXT locations{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLE_LOCATIONS_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &locations}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) { + const auto multisampleProperties = state->InstanceFunction("vkGetPhysicalDeviceMultisamplePropertiesEXT"); + for (const auto samples : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { + if ((locations.sampleLocationSampleCounts & samples) == 0) continue; + VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT}; + multisampleProperties(selected, samples, &multisample); + if (multisample.maxSampleLocationGridSize.width != 0 && multisample.maxSampleLocationGridSize.height != 0) state->sampleLocationSampleCounts |= samples; + } + } + if (state->sampleLocationSampleCounts != 0) deviceExtensions.push_back(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME); + } // Guest dispatches cover whole thread groups past the edge of small images; robust image access // drops those writes instead of faulting the device. const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); @@ -3019,6 +3035,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; context.srgbDecodeFormats = state->srgbDecodeFormats; + context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; context.singlePassStorage = state->storageImageWithoutFormat; return context; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 38049d9e6..0fa6d5c8b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -170,6 +170,7 @@ struct Context { bool provokingVertexLast = false; bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; + VkSampleCountFlags sampleLocationSampleCounts = 0; template TFunction Function(const char* name) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index bb817d437..0db0b87eb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -106,6 +106,7 @@ bool HasStencil(VkFormat format); void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); +void ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index bb18791e6..80959a49e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -116,10 +116,13 @@ struct State { VkPipelineColorBlendAttachmentState blend; std::array blendConstants; std::uint32_t samples = 1; + std::array sampleLocations{}; + bool customSampleLocations = false; }; ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); +std::array SampleLocations(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 0b5d5eb79..56005f036 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -34,6 +34,7 @@ public: const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + if ((context.sampleLocationSampleCounts & target.samples) != 0) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; info.imageType = VK_IMAGE_TYPE_2D; info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b1fe81c22..a957824c0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -50,6 +50,11 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: } mix(state.renderExtent.width); mix(state.renderExtent.height); + mix(state.samples); + mix(state.customSampleLocations); + if (state.customSampleLocations) { + for (const auto location : state.sampleLocations) mix(location); + } if (!context.provokingVertexModePerPipeline) mix(state.provokingVertexMode); return key; } @@ -1065,6 +1070,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P // Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw. ValidateViewport(context, state.viewport); ValidateDepthBounds(context, state); + ValidateSampleLocations(context, state); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; if (draw.indexed) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 27bfd7ee9..ad1caed7f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -106,6 +106,10 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } } +void ValidateSampleLocations(const Context& context, const State& state) { + Require(!state.customSampleLocations || (context.sampleLocationSampleCounts & state.samples) != 0, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); +} + void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::isfinite(viewport.minDepth) && std::isfinite(viewport.maxDepth), "non-finite viewport depth range"); Require(context.depthRangeUnrestricted || (viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1), "viewport depth outside [0, 1] requires VK_EXT_depth_range_unrestricted"); @@ -206,6 +210,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); + ValidateSampleLocations(context, state); // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -367,6 +372,11 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; samples.rasterizationSamples = static_cast(state.samples); + const auto locations = SampleLocations(state); + VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; + sampleLocations.sampleLocationsEnable = VK_TRUE; + sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, {1, 1}, state.samples, locations.data()}; + if ((context.sampleLocationSampleCounts & state.samples) != 0) samples.pNext = &sampleLocations; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -563,6 +573,16 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V begin.renderPass = renderPass; begin.framebuffer = framebuffer.Handle(); begin.renderArea = {{0, 0}, extent}; + const auto locations = SampleLocations(state); + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(state.samples), {1, 1}, state.samples, locations.data()}; + const VkAttachmentSampleLocationsEXT initial{static_cast(state.colors.size()), info}; + const VkSubpassSampleLocationsEXT post{0, info}; + VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; + sampleLocations.attachmentInitialSampleLocationsCount = state.depth ? 1 : 0; + sampleLocations.pAttachmentInitialSampleLocations = state.depth ? &initial : nullptr; + sampleLocations.postSubpassSampleLocationsCount = 1; + sampleLocations.pPostSubpassSampleLocations = &post; + if ((context.sampleLocationSampleCounts & state.samples) != 0) begin.pNext = &sampleLocations; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); Continue(commands, state); } @@ -687,6 +707,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat for (const auto& blend : state.blends) append(key, blend); for (const auto value : state.blendConstants) append(key, value); append(key, state.samples); + append(key, state.customSampleLocations); + if (state.customSampleLocations) append(key, state.sampleLocations); append(key, state.colors.size()); for (const auto& color : state.colors) append(key, color.format); if (state.blends.size() != state.colors.size()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 3316fd778..6a089940e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -327,7 +327,12 @@ std::uint32_t decodeSamples(const Registers& cx) { } Require(log2 <= 3u, "more than eight samples are unsupported"); Require(((config >> 20u) & 7u) == log2, "exposing fewer samples than are rasterized is unsupported"); - const auto samples = 1u << log2; + return 1u << log2; +} + +void decodeSampleLocations(const Registers& cx, State& state) { + const auto samples = state.samples; + if (samples == 1) return; const std::span> standard = samples == 2 ? std::span>(StandardLocations2) : samples == 4 ? std::span>(StandardLocations4) : std::span>(StandardLocations8); for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { for (std::uint32_t sample = 0; sample < samples; ++sample) { @@ -335,10 +340,14 @@ std::uint32_t decodeSamples(const Registers& cx) { const auto bits = (word >> (8u * (sample % 4u))) & 0xffu; const auto x = static_cast(static_cast(bits << 4u)) >> 4; const auto y = static_cast(static_cast(bits & 0xf0u)) >> 4; - Require(x == standard[sample][0] && y == standard[sample][1], "nonstandard sample locations are unsupported"); + if (pixel == 0) { + state.sampleLocations[sample] = static_cast(bits); + state.customSampleLocations |= x != standard[sample][0] || y != standard[sample][1]; + } else { + Require(bits == state.sampleLocations[sample], "nonstandard sample locations varying between pixels are unsupported"); + } } } - return samples; } bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { @@ -519,6 +528,23 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse } +std::array SampleLocations(const State& state) { + Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count"); + std::array locations{}; + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + std::array position{}; + if (state.customSampleLocations) { + const auto bits = state.sampleLocations[sample]; + position = {static_cast(bits & 7u) - static_cast(bits & 8u), static_cast((bits >> 4u) & 7u) - static_cast((bits >> 4u) & 8u)}; + } else if (state.samples != 1) { + const auto& standard = state.samples == 2 ? StandardLocations2[sample] : state.samples == 4 ? StandardLocations4[sample] : StandardLocations8[sample]; + position = {standard[0], standard[1]}; + } + locations[sample] = {0.5f + position[0] / 16.0f, 0.5f + position[1] / 16.0f}; + } + return locations; +} + ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto value = read(queue.context, 0x2d5); const auto validate = [&](bool condition, const char* reason) { @@ -619,6 +645,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); result.samples = decodeSamples(cx); + decodeSampleLocations(cx, result); { const auto depthControl = read(cx, 0x200); const auto renderControl = find(cx, 0x000); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 0103b5aec..21c8ab46d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1752,6 +1752,41 @@ void multisampleTests() { } state = DecodeState(queue); Require(state.samples == 8 && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); + queue.context[0x2f8] = 0x00200002; + queue.context[0x31d] = 0x12000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a; + state = DecodeState(queue); + Require(state.samples == 4 && state.customSampleLocations, "uniform custom sample locations were not decoded"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + const std::array expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}}; + for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(locations[sample].x == expected[sample].x && locations[sample].y == expected[sample].y, "custom sample coordinates or indices changed"); + AgcDriver::Graphics::Context sampleContext{}; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "VK_EXT_sample_locations"); + sampleContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "sample count"); + sampleContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; + AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; + state = DecodeState(queue); + Require(!state.customSampleLocations, "standard sample locations require the extension"); + AgcDriver::Graphics::ValidateSampleLocations({}, state); + for (const auto samples : {2u, 8u}) { + queue = multisampled(); + const auto log2 = samples == 2 ? 1u : 3u; + queue.context[0x2f8] = (log2 << 20u) | log2; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0x77887788; + queue.context[0x2ff + pixel * 4u] = 0x77887788; + } + state = DecodeState(queue); + Require(state.customSampleLocations && state.samples == samples, "a custom 2x or 8x pattern did not decode"); + const auto endpoints = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto expected = sample % 2u == 0 ? 0.0f : 15.0f / 16.0f; + Require(endpoints[sample].x == expected && endpoints[sample].y == expected, "signed sample coordinate endpoints changed"); + } + } queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 51f08cc9e..8a389c755 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -555,6 +555,7 @@ Throughout the project, every function at every stage either **does exactly what - [Multisampled CMASK state](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled targets in CB passes](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve is modeled. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) is taken as `max(CB bytes * samples, width * height * element bytes * samples)` from its address, an upper bound on the sample planes of a target laid out in 64 KiB blocks (inferred: the single-sample layout size times the sample count; the exact fragment-aware footprint of addrlib is not computed, so a few bytes past the real footprint can throw too). Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. +- [Custom MSAA sample locations](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. From e28dcc0c1263f4c68e559b60bbbd526f6e96b056 Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:13:44 -0700 Subject: [PATCH 213/318] fix(agc): validate custom sample location depth formats --- .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/include/Pipeline.hpp | 3 +- .../Graphics/src/DepthSurface.cpp | 12 +++++- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 - .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 13 ++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 37 +++++++++++++++++++ 6 files changed, 59 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index f5b6cad4a..3d493123d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -13,6 +13,7 @@ namespace AgcDriver::Graphics { class Texture; VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 0db0b87eb..cfeb4ba77 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -87,6 +87,7 @@ private: VkFormat depthFormat = VK_FORMAT_UNDEFINED; VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_UNDEFINED; std::vector colorSlots; + bool sampleLocationsEnabled = false; std::vector framebuffers; }; @@ -106,7 +107,7 @@ bool HasStencil(VkFormat format); void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); -void ValidateSampleLocations(const Context& context, const State& state); +bool ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 56005f036..6b0383da7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -34,7 +34,7 @@ public: const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - if ((context.sampleLocationSampleCounts & target.samples) != 0) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; + if (SampleLocationsCompatibleDepth(context, target.format, target.samples)) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; info.imageType = VK_IMAGE_TYPE_2D; info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; @@ -188,6 +188,16 @@ std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel) { return width * height * bytesPerTexel; } +bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples) { + if (samples <= 1 || (context.sampleLocationSampleCounts & samples) == 0) return false; + VkImageFormatProperties properties{}; + const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, &properties); + if (result == VK_ERROR_FORMAT_NOT_SUPPORTED) return false; + Check(result, "vkGetPhysicalDeviceImageFormatProperties sample locations depth"); + return (properties.sampleCounts & samples) != 0; +} + VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { std::lock_guard lock(surfacesMutex()); const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index a957824c0..5c1b7f82a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1070,7 +1070,6 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P // Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw. ValidateViewport(context, state.viewport); ValidateDepthBounds(context, state); - ValidateSampleLocations(context, state); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; if (draw.indexed) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index ad1caed7f..e61ba99f4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include @@ -106,8 +107,10 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } } -void ValidateSampleLocations(const Context& context, const State& state) { - Require(!state.customSampleLocations || (context.sampleLocationSampleCounts & state.samples) != 0, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); +bool ValidateSampleLocations(const Context& context, const State& state) { + const bool enabled = (context.sampleLocationSampleCounts & state.samples) != 0 && (!state.depth || SampleLocationsCompatibleDepth(context, state.depth->format, state.depth->samples)); + Require(!state.customSampleLocations || enabled, "custom sample locations require a compatible depth format and sample count"); + return enabled; } void ValidateViewport(const Context& context, const VkViewport& viewport) { @@ -210,7 +213,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); - ValidateSampleLocations(context, state); + sampleLocationsEnabled = ValidateSampleLocations(context, state); // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -376,7 +379,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; sampleLocations.sampleLocationsEnable = VK_TRUE; sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, {1, 1}, state.samples, locations.data()}; - if ((context.sampleLocationSampleCounts & state.samples) != 0) samples.pNext = &sampleLocations; + if (sampleLocationsEnabled) samples.pNext = &sampleLocations; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -582,7 +585,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V sampleLocations.pAttachmentInitialSampleLocations = state.depth ? &initial : nullptr; sampleLocations.postSubpassSampleLocationsCount = 1; sampleLocations.pPostSubpassSampleLocations = &post; - if ((context.sampleLocationSampleCounts & state.samples) != 0) begin.pNext = &sampleLocations; + if (sampleLocationsEnabled) begin.pNext = &sampleLocations; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); Continue(commands, state); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 21c8ab46d..d0f00ee39 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1721,6 +1721,18 @@ void uint16ExportTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8"); } +VkSampleCountFlags depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; +VkResult depthSampleResult = VK_SUCCESS; +std::uint32_t depthSampleQueries = 0; + +VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, VkFormat format, VkImageType type, VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags, VkImageFormatProperties* properties) { + Require(format == VK_FORMAT_D32_SFLOAT && type == VK_IMAGE_TYPE_2D && tiling == VK_IMAGE_TILING_OPTIMAL, "sample location query changed the depth image format or type"); + Require(usage == (VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT) && flags == VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, "sample location query differs from depth image creation"); + ++depthSampleQueries; + properties->sampleCounts = depthSampleCounts; + return depthSampleResult; +} + void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorMetadataPass; @@ -1766,9 +1778,34 @@ void multisampleTests() { expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "sample count"); sampleContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); + sampleContext.imageFormatProperties = sampleLocationDepthProperties; + using AgcDriver::Graphics::SampleLocationsCompatibleDepth; + Require(SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "supported custom sample depth format was refused"); + state.depth = AgcDriver::Graphics::DepthTarget{}; + state.depth->format = VK_FORMAT_D32_SFLOAT; + state.depth->samples = 4; + Require(AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "supported custom depth sample locations were disabled"); + depthSampleCounts = VK_SAMPLE_COUNT_2_BIT; + Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth count was accepted"); + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); + depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; + depthSampleResult = VK_ERROR_FORMAT_NOT_SUPPORTED; + Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth format was accepted"); + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); + state.customSampleLocations = false; + Require(!AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "standard locations did not fall back for an incompatible depth format"); + depthSampleResult = VK_ERROR_OUT_OF_HOST_MEMORY; + expectFailure([&] { SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4); }, "Vulkan result"); + depthSampleResult = VK_SUCCESS; + const auto queries = depthSampleQueries; + Require(!SampleLocationsCompatibleDepth({}, VK_FORMAT_D32_SFLOAT, 4) && !SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 1), "sample location depth compatibility requires the extension and multisampling"); + Require(depthSampleQueries == queries, "ineligible sample location depth formats were queried"); for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; state = DecodeState(queue); Require(!state.customSampleLocations, "standard sample locations require the extension"); + const auto standard = AgcDriver::Graphics::SampleLocations(state); + const std::array expectedStandard{{{0.375f, 0.125f}, {0.875f, 0.375f}, {0.125f, 0.625f}, {0.625f, 0.875f}}}; + for (std::size_t sample = 0; sample < expectedStandard.size(); ++sample) Require(standard[sample].x == expectedStandard[sample].x && standard[sample].y == expectedStandard[sample].y, "standard sample coordinate scale or Y direction changed"); AgcDriver::Graphics::ValidateSampleLocations({}, state); for (const auto samples : {2u, 8u}) { queue = multisampled(); From 9bc72d105ee604937778870fd1bb933c2d68229a Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:05:05 -0700 Subject: [PATCH 214/318] fix(agc): preserve graphics behavior after rebase Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 27 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index e61ba99f4..a8f77a1af 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -108,7 +108,9 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } bool ValidateSampleLocations(const Context& context, const State& state) { - const bool enabled = (context.sampleLocationSampleCounts & state.samples) != 0 && (!state.depth || SampleLocationsCompatibleDepth(context, state.depth->format, state.depth->samples)); + const bool extensionSupportsSamples = (context.sampleLocationSampleCounts & state.samples) != 0; + Require(!state.customSampleLocations || extensionSupportsSamples, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); + const bool enabled = extensionSupportsSamples && (!state.depth || SampleLocationsCompatibleDepth(context, state.depth->format, state.depth->samples)); Require(!state.customSampleLocations || enabled, "custom sample locations require a compatible depth format and sample count"); return enabled; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d0f00ee39..c0922a655 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1741,13 +1741,19 @@ void multisampleTests() { auto queue = makeState(); queue.context[0x2f8] = 0x00100001; queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; queue.context[0x31d] = 0x9000; - queue.context[0x3b8] |= (0x18u << 19u) | 0x4000000u; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; return queue; }; + const auto cmaskAddress = reinterpret_cast(cmaskMemory.data()); + const auto setCmaskAddress = [&](auto& target, std::uint32_t slot = 0u) { + const auto stride = slot * 0xfu; + target.context[0x31f + stride] = static_cast(cmaskAddress >> 8u); + target.context[0x398 + slot] = static_cast(cmaskAddress >> 40u); + }; auto queue = multisampled(); auto state = DecodeState(queue); Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.colors[0].cmaskAddress == 0, "2x multisampling did not decode"); @@ -1837,28 +1843,28 @@ void multisampleTests() { expectFailure([&] { DecodeState(queue); }, "coverage conversion"); queue = multisampled(); queue.context[0x31c] |= 0x6000; - queue.context[0x31f] = 0x1234; - Require(DecodeState(queue).colors[0].cmaskAddress == 0x123400, "the CMASK address of a multisampled target did not decode"); + setCmaskAddress(queue); + Require(DecodeState(queue).colors[0].cmaskAddress == cmaskAddress, "the CMASK address of a multisampled target did not decode"); queue = makeState(); queue.context[0x31c] |= 0x4000; expectFailure([&] { DecodeState(queue); }, "color compression"); queue = multisampled(); queue.context[0x31c] |= 0x2000; - queue.context[0x31f] = 0x1234; + setCmaskAddress(queue); const auto fastCleared = DecodeState(queue).colors[0]; - Require(fastCleared.cmaskAddress == 0x123400 && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); + Require(fastCleared.cmaskAddress == cmaskAddress && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); queue = multisampled(); queue.context[0x31c] |= 0x4000; - queue.context[0x31f] = 0x1234; + setCmaskAddress(queue); const auto compressed = DecodeState(queue).colors[0]; - Require(compressed.cmaskAddress == 0x123400 && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); + Require(compressed.cmaskAddress == cmaskAddress && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); queue = multisampled(); queue.context[0x31c] |= 0x2000; queue.context[0x31f] = 0; expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); queue.context[0x31c] |= 0x2000; - queue.context[0x31f] = 0x1234; + setCmaskAddress(queue); std::vector reads; AgcDriver::Graphics::RegisterReadLog() = &reads; static_cast(DecodeState(queue)); @@ -1872,6 +1878,8 @@ void multisampleTests() { queue.context[0x32c] = 0; queue.context[0x3b1] = queue.context[0x3b0]; queue.context[0x3b9] = queue.context[0x3b8]; + setCmaskAddress(queue); + setCmaskAddress(queue, 1); auto pass = DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode"); queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); @@ -1884,8 +1892,7 @@ void multisampleTests() { queue.context[0x3b1] = queue.context[0x3b0]; queue.context[0x3b9] = queue.context[0x3b8]; queue.context[0x32b] = queue.context[0x31c] | 0x2000u; - queue.context[0x32e] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 8u); - queue.context[0x399] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 40u); + setCmaskAddress(queue, 1); expectFailure([&] { DecodeColorMetadataPass(queue); }, "fast-clear color target"); queue = multisampled(); queue.context[0x2f8] = 0; From 33efe5ac5d010c667c81a5972a557656d38cc128 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 11 Oct 2026 09:59:05 +0200 Subject: [PATCH 215/318] fix(agc): the standard 8x pattern's last sample is at (7, -7) Vulkan's standard 8x sample 7 is (0.9375, 0.0625), (7, -7) in signed sixteenths; the table had (7, -8), so the standard pattern (PPSA99902's PA_SC_AA_SAMPLE_LOCS words 0xbd153fd1 / 0x9773f95b) was refused as nonstandard. Regression row with those words. Reported by @Amoenus. Co-Authored-By: Amoenus <5391396+Amoenus@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 9a1a0672cac0f8e3dcaa6e28cbe6b7b684a40a1f) --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c0922a655..0d5ddb92e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1830,6 +1830,15 @@ void multisampleTests() { Require(endpoints[sample].x == expected && endpoints[sample].y == expected, "signed sample coordinate endpoints changed"); } } + queue.context[0x2f8] = 0x00300003; + queue.context[0x31d] = 0x1b000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + state = DecodeState(queue); + Require(state.samples == 8 && !state.customSampleLocations && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); + AgcDriver::Graphics::ValidateSampleLocations({}, state); queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); From fb7615e3078cb4081c89a60a39f771b4dab27bca Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:15:34 -0700 Subject: [PATCH 216/318] fix(agc): key GPL libraries by custom sample locations --- .../prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 8 ++++++++ core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index a8f77a1af..dcc557fd5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -197,6 +197,14 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); appendKey(keys.fragmentShader, state.samples); appendKey(keys.fragmentOutput, state.samples); + appendKey(keys.fragmentShader, state.customSampleLocations); + appendKey(keys.fragmentOutput, state.customSampleLocations); + if (state.customSampleLocations) { + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + appendKey(keys.fragmentShader, state.sampleLocations[sample]); + appendKey(keys.fragmentOutput, state.sampleLocations[sample]); + } + } for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend); for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value); appendKey(keys.renderPass, state.blends.size()); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 5484435e9..7cdb976a1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5292,6 +5292,19 @@ void pipelineLibraryTests(const Device& device) { state.samples = 1; state.colors.front().samples = 1; Require(lookup() != multisampled && counters().built == std::array{2, 2, 4, 4}, "the 1x pipeline after a 4x one built its libraries again"); + if ((context.sampleLocationSampleCounts & VK_SAMPLE_COUNT_4_BIT) != 0) { + state.samples = 4; + state.colors.front().samples = 4; + state.customSampleLocations = true; + state.sampleLocations = {0x10, 0x21, 0x32, 0x43}; + const auto pattern = lookup(); + const auto beforePatternChange = counters().built; + state.sampleLocations[0] = 0x20; + const auto otherPattern = lookup(); + Require(otherPattern != pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u && counters().built[0] == beforePatternChange[0] && counters().built[1] == beforePatternChange[1], "a custom sample pattern did not rebuild exactly the fragment shader and fragment output libraries"); + state.sampleLocations[0] = 0x10; + Require(lookup() == pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u, "a cached custom sample pattern was not reused"); + } WaitForOptimizedPipelines(context.device); const auto lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr && context.pipelineLinkTimeOptimization; Require(counters().optimized == (lto ? counters().linked : 0u), "a linked pipeline was not optimized in the background"); From 8f3389727577e7e8f7a7c9c36b0bb0087aa86327 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 12 Oct 2026 05:37:35 +0200 Subject: [PATCH 217/318] fix(libs): allow socket receives into watched guest memory --- core/libs/prx/libSceNet/CMakeLists.txt | 13 ++ core/libs/prx/libSceNet/Export.cpp | 5 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 + core/libs/tests/GuestSocketHostWrite.cpp | 256 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 5 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestSocketHostWrite.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index bcf7a8ccf..98776d4b5 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -15,6 +15,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + if(WIN32) + add_test_executable(guest_socket_host_write_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketHostWrite.cpp) + target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_host_write_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_socket_host_write_tests) + foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom) + add_test(NAME guest_socket_host_write_${api}_udp COMMAND guest_socket_host_write_tests ${api}) + add_test(NAME guest_socket_host_write_${api}_tcp COMMAND guest_socket_host_write_tests ${api} tcp) + set_tests_properties(guest_socket_host_write_${api}_udp guest_socket_host_write_${api}_tcp + PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endforeach() + endif() add_test_executable(guest_sce_net_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNet.cpp) target_include_directories(guest_sce_net_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 8203f4edc..0f30e9c85 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -32,6 +32,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -644,6 +645,8 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); + if (!destination.Open()) return fail(NET_EFAULT); const int native_flags = flags == 2 ? MSG_PEEK : 0; const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); #ifdef _WIN32 @@ -664,6 +667,8 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); + if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); const int native_flags = flags == 2 ? MSG_PEEK : 0; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 0ac8f3163..a6dfc65da 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -605,6 +605,8 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if ((flags & ~2) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); + const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); + if (!destination.Open()) return Fail(14); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), flags & 2 ? MSG_PEEK : 0); #ifdef _WIN32 @@ -699,6 +701,8 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if ((flags & ~2) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if ((!buffer && length) || (address && !addressLength)) return Fail(14); + const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); + if (!destination.Open()) return Fail(14); sockaddr_storage native{}; socklen_t size = sizeof(native); auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp new file mode 100644 index 000000000..f34c742a1 --- /dev/null +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -0,0 +1,256 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +int APS5_VABI sceNetSocketClose(int); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelMprotect(void*, std::size_t, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +static DWORD Protection(const void* address) { + MEMORY_BASIC_INFORMATION information{}; + Require(VirtualQuery(address, &information, sizeof(information)) != 0, "query host protection"); + return information.Protect; +} + +int main(int argc, char** argv) { + constexpr std::size_t page = 0x4000; + constexpr std::size_t capacity = 32; + constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom"}; + Require(argc == 2 || argc == 3, "receive API argument required"); + Require(argc != 3 || std::strcmp(argv[2], "tcp") == 0, "unknown transport"); + const bool stream = argc == 3; + const auto selected = std::find_if(std::begin(names), std::end(names), [&](const char* name) { + return std::strcmp(name, argv[1]) == 0; + }); + Require(selected != std::end(names), "unknown receive API"); + const auto api = selected - std::begin(names); + const bool net = api >= 4; + const bool from = api == 1 || api == 5; + const bool peek = api != 2 && api != 3; + const auto create = [&] { + return net ? sceNetSocket(nullptr, 2, stream ? 1 : 2, stream ? 6 : 17) : + socket_nid_postfix(2, stream ? 1 : 2, stream ? 6 : 17); + }; + int receiver = create(); + const int sender = create(); + Require(receiver >= 0 && sender >= 0, "create sockets"); + std::array destination{16, 2, 0, 0, 127, 0, 0, 1}; + Require((net ? sceNetBind_nid_postfix(receiver, destination.data(), destination.size()) : + bind_nid_postfix(receiver, destination.data(), destination.size())) == 0, "bind receiver"); + auto senderAddress = destination; + Require((net ? sceNetBind_nid_postfix(sender, senderAddress.data(), senderAddress.size()) : + bind_nid_postfix(sender, senderAddress.data(), senderAddress.size())) == 0, "bind sender"); + std::uint32_t size = destination.size(); + Require((net ? sceNetGetsockname(receiver, destination.data(), &size) : + getsockname_nid_postfix(receiver, destination.data(), &size)) == 0, "receiver address"); + size = senderAddress.size(); + Require((net ? sceNetGetsockname(sender, senderAddress.data(), &size) : + getsockname_nid_postfix(sender, senderAddress.data(), &size)) == 0, "sender address"); + if (stream) { + Require((net ? sceNetListen(receiver, 1) : listen_nid_postfix(receiver, 1)) == 0, "listen on loopback"); + Require((net ? sceNetConnect(sender, destination.data(), destination.size()) : + connect_nid_postfix(sender, destination.data(), destination.size())) == 0, "connect sender"); + const int accepted = net ? sceNetAccept(receiver, nullptr, nullptr) : accept_nid_postfix(receiver, nullptr, nullptr); + Require(accepted >= 0, "accept sender"); + Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close listener"); + receiver = accepted; + } + const auto nonblocking = [&](bool enabled) { + const int value = enabled; + Require((net ? sceNetSetsockopt(receiver, 0xffff, 0x1200, &value, sizeof(value)) : + fcntl_nid_postfix(receiver, 4, enabled ? 4 : 0)) == 0, "set receiver mode"); + }; + nonblocking(true); + const char payload[] = "watched socket data"; + const auto send = [&] { + Require((net ? sceNetSendto(sender, payload, sizeof(payload), 0, stream ? nullptr : destination.data(), + stream ? 0 : destination.size()) : sendto_nid_postfix(sender, payload, sizeof(payload), 0, + stream ? nullptr : destination.data(), stream ? 0 : destination.size())) == sizeof(payload), + "send packet"); + }; + const auto receive = [&](void* buffer, std::size_t bytes, int flags, void* address, std::uint32_t* addressLength) { + switch (api) { + case 0: return recv_nid_postfix(receiver, buffer, bytes, flags); + case 1: return recvfrom_nid_postfix(receiver, buffer, bytes, flags, address, addressLength); + case 2: return read_nid_postfix(receiver, buffer, bytes); + case 3: return sceKernelRead(receiver, buffer, bytes); + case 4: return sceNetRecv(receiver, buffer, bytes, flags); + default: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + } + }; + const auto waitForPacket = [&] { + std::array buffer{}; + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = net ? sceNetRecv(receiver, buffer.data(), buffer.size(), 2) : + recv_nid_postfix(receiver, buffer.data(), buffer.size(), 2); + if (result == sizeof(payload) || (result < 0 && *error != 35)) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == sizeof(payload), "wait for complete packet"); + }; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &physical) == 0, "allocate direct memory"); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, page * 2, 3, 0, physical, 0) == 0, "map read/write memory"); + auto* buffer = static_cast(mapping) + page - 7; + const auto collect = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(mapping), page * 2, + pages.data(), &count, true), "collect tracked writes"); + return count; + }; + const auto reset = [&] { + const GuestArena::HostWrite destination(buffer - 1, capacity + 2); + Require(destination.Open(), "reset writable receive buffer"); + std::memset(buffer - 1, '!', capacity + 2); + }; + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + for (const std::size_t bytes : {std::size_t{0}, std::size_t{4}, sizeof(payload), capacity}) { + send(); + waitForPacket(); + for (const int flags : {2, 0}) { + if (flags == 2 && !peek) continue; + reset(); + collect(); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "write tracking arms both guest pages"); + std::array address{}; + size = address.size(); + *error = 123; + const auto result = receive(buffer, bytes, flags, stream ? nullptr : address.data(), stream ? nullptr : &size); + const auto expected = std::min(bytes, sizeof(payload)); + if (result != static_cast(expected)) { + std::fprintf(stderr, "%s capacity=%zu flags=%d result=%lld errno=%d\n", argv[1], bytes, flags, + static_cast(result), *error); + return 1; + } + Require(*error == 123, "successful receive preserves errno"); + Require(buffer[-1] == '!' && std::memcmp(buffer, payload, expected) == 0 && + std::all_of(buffer + expected, buffer + capacity + 1, [](char value) { return value == '!'; }), + "received data and buffer guards"); + if (from && !stream) Require(size == senderAddress.size() && std::memcmp(address.data(), senderAddress.data(), size) == 0, + "sender address preserved"); + if (flags == 0 && ((bytes == 0 && !peek) || (stream && expected < sizeof(payload)))) { + std::array retained{}; + Require(receive(retained.data(), retained.size(), 0, nullptr, nullptr) == sizeof(payload) - expected && + std::memcmp(retained.data(), payload + expected, sizeof(payload) - expected) == 0, + "short stream receive or zero-byte read retains unread data"); + } + if (bytes != 0) Require(collect() != 0, "receive reported as a tracked write"); + collect(); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "receive releases the host write guard"); + } + } + reset(); + collect(); + *error = 0; + const auto empty = receive(buffer, capacity, 0, nullptr, nullptr); + const auto expectedAgain = api == 3 ? static_cast(0x80020023u) : + net ? static_cast(0x80410123u) : -1; + Require(empty == expectedAgain && *error == 35, "empty nonblocking receive reports EAGAIN"); + collect(); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "failed receive releases the host write guard"); + Require(sceKernelMprotect(mapping, page * 2, 1) == 0, "make guest memory read-only"); + send(); + waitForPacket(); + *error = 0; + const auto rejected = receive(buffer, capacity, 0, nullptr, nullptr); + const auto expectedError = api == 3 ? static_cast(0x8002000eu) : + net ? static_cast(0x8041010eu) : -1; + Require(rejected == expectedError && *error == 14, "read-only destination fails with EFAULT"); + Require(std::all_of(buffer - 1, buffer + capacity + 1, [](char value) { return value == '!'; }), + "read-only destination unchanged"); + std::array recovery{}; + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload) && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "rejected write preserves queued packet"); + send(); + waitForPacket(); + *error = 123; + Require(receive(buffer, 0, peek ? 2 : 0, nullptr, nullptr) == 0 && *error == 123, + "zero-byte receive does not require a writable destination"); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "zero-byte receive preserves read-only protection"); + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload) && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "zero-byte peek or read retains queued data"); + Require(sceKernelMprotect(mapping, page * 2, 3) == 0, "restore guest write permission"); + reset(); + collect(); + nonblocking(false); + std::int64_t result = -1; + int workerError = 0; + std::thread worker([&] { + int* slot = net ? sceNetErrnoLoc() : __error_nid_postfix(); + *slot = 123; + result = receive(buffer, capacity, 0, nullptr, nullptr); + workerError = *slot; + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (Protection(buffer) != PAGE_READWRITE && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(Protection(buffer) == PAGE_READWRITE, "blocking receive opens its destination"); + collect(); + Require(Protection(buffer) == PAGE_READWRITE && Protection(buffer + capacity - 1) == PAGE_READWRITE, + "collection does not rearm an in-flight receive"); + send(); + worker.join(); + Require(result == sizeof(payload) && workerError == 123 && std::memcmp(buffer, payload, sizeof(payload)) == 0, + "blocking receive completes and preserves errno"); + Require(collect() != 0, "completion remains dirty after an in-flight collection"); + Require(Protection(buffer) == PAGE_READONLY, "completed receive can be rearmed"); + Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); + Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); + Require(sceKernelMunmap(mapping, page * 2) == 0, "unmap direct memory"); + Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0, "release direct memory"); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 51f08cc9e..f5fc1b807 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -416,7 +416,7 @@ Throughout the project, every function at every stage either **does exactly what - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: - - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (the file reads); a host call that writes guest memory without it (sockets, `clock_gettime` and the like into a watched page) fails; + - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; - the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same. - [Pending storage-image results](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented. From c6be0e95ccec3794233210439fad18e5f285d0b3 Mon Sep 17 00:00:00 2001 From: codingreapermaps <233954859+codingreapermaps@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:17:00 +0100 Subject: [PATCH 218/318] fix(libkernel_sys): read relative symbolic links on Windows readlink threw for every Windows reparse point, including the links symlink creates. Read the target of a relative symbolic link from its reparse data and return it with forward slashes. Absolute targets, junctions and other reparse points still throw. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 58 +++++++++++++++++++++++--- core/libs/tests/GuestKernelSys.cpp | 31 ++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index ffe9d557d..a50b7c0bb 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -63,6 +64,50 @@ int GuestError(const std::error_code& error) { const int value = condition.value(); return value > 0 && value <= 34 ? value : GuestEio; } + +std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t size) { + const auto count = std::min(size, target.size()); + if (count != 0 && buffer == nullptr) return Fail(GuestEfault); + if (count != 0) std::memcpy(buffer, target.data(), count); + return static_cast(count); +} + +#ifdef _WIN32 +struct SymbolicLinkReparseData { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + ULONG flags; +}; +constexpr ULONG SymbolicLinkRelative = 1; + +bool ReadRelativeSymbolicLink(const std::filesystem::path& native, std::string& target) { + const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "readlink: CreateFileW"); + std::array data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) throw std::system_error(static_cast(code), std::system_category(), "readlink: FSCTL_GET_REPARSE_POINT"); + SymbolicLinkReparseData header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & SymbolicLinkRelative) == 0) return false; + const std::size_t begin = sizeof(header) + header.substituteNameOffset; + if (begin + header.substituteNameLength > size) throw std::runtime_error("readlink: malformed symbolic link reparse data"); + std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); + std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); + target = std::filesystem::path(name).generic_string(); + return true; +} +#endif } extern "C" { @@ -95,8 +140,12 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { - NotImplemented_nid_no_patch("readlink of a Windows symbolic link"); - return -1; + std::string target; + if (!ReadRelativeSymbolicLink(native, target)) { + NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); + return -1; + } + return CopyLinkTarget(target, buffer, size); } #endif std::error_code error; @@ -106,10 +155,7 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); const auto target = std::filesystem::read_symlink(native, error).generic_string(); if (error) return Fail(GuestError(error)); - const auto count = std::min(size, target.size()); - if (count != 0 && buffer == nullptr) return Fail(GuestEfault); - if (count != 0) std::memcpy(buffer, target.data(), count); - return static_cast(count); + return CopyLinkTarget(target, buffer, size); } int APS5_VABI link_nid_postfix(const char* existing, const char* created) { diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 8803d085c..8483b1be8 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -8,9 +8,13 @@ #include #include #include +#include #include #ifdef _WIN32 #include +#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE +#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 +#endif #endif #ifdef __linux__ #include @@ -161,6 +165,33 @@ int main() { Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == 0); Require((GetFileAttributesW(std::filesystem::path(dangling).wstring().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); + Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); + Require(readlink_nid_postfix(dangling.c_str(), buffer, sizeof(buffer)) == 11 && std::memcmp(buffer, "missing.bin", 11) == 0); + const auto nested = (root / "nested").string(); + Require(symlink_nid_postfix("sub/target.txt", nested.c_str()) == 0); + std::memset(buffer, '#', sizeof(buffer)); + Require(readlink_nid_postfix(nested.c_str(), buffer, sizeof(buffer)) == 14); + Require(std::memcmp(buffer, "sub/target.txt", 14) == 0 && buffer[14] == '#'); + Require(readlink_nid_postfix(nested.c_str(), buffer, 3) == 3 && std::memcmp(buffer, "sub", 3) == 0); + Require(readlink_nid_postfix(nested.c_str(), nullptr, 0) == 0); + Require(readlink_nid_postfix(nested.c_str(), nullptr, sizeof(buffer)) == -1 && Error() == 14); + const auto accented = (root / "accented").string(); + Require(symlink_nid_postfix("a\xc3\xa7\xc3\xa3o.bin", accented.c_str()) == 0); + Require(readlink_nid_postfix(accented.c_str(), buffer, sizeof(buffer)) == 10 && std::memcmp(buffer, "a\xc3\xa7\xc3\xa3o.bin", 10) == 0); + Require(std::filesystem::create_directory(root / "sub")); + const auto directoryLink = (root / "directory-link").string(); + Require(symlink_nid_postfix("sub", directoryLink.c_str()) == 0); + Require(readlink_nid_postfix(directoryLink.c_str(), buffer, sizeof(buffer)) == 3 && std::memcmp(buffer, "sub", 3) == 0); + const auto absolute = std::filesystem::absolute(root / "absolute"); + Require(CreateSymbolicLinkW(absolute.wstring().c_str(), std::filesystem::absolute(original).wstring().c_str(), + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + bool refused = false; + try { + readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)); + } catch (const std::runtime_error& failure) { + refused = std::string(failure.what()) == "readlink of a Windows reparse point other than a relative symbolic link not implemented"; + } + Require(refused); #else Require(std::filesystem::is_symlink(std::filesystem::symlink_status(symbolic))); Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 51f08cc9e..83d9930e9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -390,6 +390,7 @@ Throughout the project, every function at every stage either **does exactly what - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [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. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and 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 +- [readlink](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target and a junction throw, as a host path has no guest form. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [Windows file identity](../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - On Linux, [direct memory](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [Per-face depth bias](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. From 605741d39f2e272963fe6db053e290555432c1dc Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:43:05 -0700 Subject: [PATCH 219/318] fix(agc): key GPL libraries by sample-location enablement --- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 3 +++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index dcc557fd5..139cff586 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -197,6 +197,9 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); appendKey(keys.fragmentShader, state.samples); appendKey(keys.fragmentOutput, state.samples); + const auto sampleLocationsEnabled = ValidateSampleLocations(context, state); + appendKey(keys.fragmentShader, sampleLocationsEnabled); + appendKey(keys.fragmentOutput, sampleLocationsEnabled); appendKey(keys.fragmentShader, state.customSampleLocations); appendKey(keys.fragmentOutput, state.customSampleLocations); if (state.customSampleLocations) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 7cdb976a1..5f194e197 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5304,6 +5304,24 @@ void pipelineLibraryTests(const Device& device) { Require(otherPattern != pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u && counters().built[0] == beforePatternChange[0] && counters().built[1] == beforePatternChange[1], "a custom sample pattern did not rebuild exactly the fragment shader and fragment output libraries"); state.sampleLocations[0] = 0x10; Require(lookup() == pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u, "a cached custom sample pattern was not reused"); + + std::optional compatibleDepth, incompatibleDepth; + state.customSampleLocations = false; + for (const auto format : {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + state.depth = DepthTarget{}; + state.depth->format = format; + state.depth->samples = state.samples; + (ValidateSampleLocations(context, state) ? compatibleDepth : incompatibleDepth) = format; + } + if (compatibleDepth && incompatibleDepth) { + state.depth->format = *compatibleDepth; + const auto compatible = lookup(); + const auto beforeDepthChange = counters().built; + state.depth->format = *incompatibleDepth; + const auto incompatible = lookup(); + Require(incompatible != compatible && counters().built[2] == beforeDepthChange[2] + 1u && counters().built[3] == beforeDepthChange[3] + 1u, "depth-format sample-location enablement did not rebuild both fragment libraries"); + } + state.customSampleLocations = true; } WaitForOptimizedPipelines(context.device); const auto lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr && context.pipelineLinkTimeOptimization; From c6f934a3d05934b9f41f7346f3fcebd07d062a48 Mon Sep 17 00:00:00 2001 From: poonasor Date: Sat, 10 Oct 2026 21:50:23 -0400 Subject: [PATCH 220/318] fix(relinker): move a short guest TLS access's follower branch into its stub A short fs-prefixed TLS access is patched with a 5-byte jump by relocating the following instructions into the stub, but any branch there was rejected outright. NHL 27 hits this on every stack-protector epilogue: fs:xor rax,[rbx] immediately followed by jne __stack_chk_fail. Allow relative conditional branches and short jumps: re-encode rel8 forms as their rel32 equivalents, rebase the displacement when the stub is emitted, and keep rejecting loop/jrcxz, calls, returns, and segment-prefixed followers. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 2 +- .../src/windows/WindowsTlsBuilder.cpp | 31 +++++++++++++++++-- .../tests/test_tls_function_coverage.py | 25 ++++++++++++++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4e8e1a4ad..5c4c4396f 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -345,7 +345,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) - set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads rejected) + set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected) foreach(register IN ITEMS 0 1 2 3 5 6 7 8 9 10 11 12 13 14 15) list(APPEND tlsCoverageGroups displacement_r${register}) endforeach() diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index df1ce0fea..e37c631dc 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -23,6 +23,8 @@ struct MovedInstruction { std::uint32_t Rva; std::vector Bytes; std::optional RipDisplacement; + std::optional BranchTarget; + std::size_t BranchDisplacementOffset = 0; }; struct TlsAccess { @@ -145,9 +147,28 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (length >= header.FileSize - offset) throw Domain::RelinkerException("Short guest TLS instruction ends its segment", header.Offset + offset); const auto next = decoder.DecodeInstruction(bytes + length, header.FileSize - offset - length); - if (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0) + const bool relativeBranch = + (next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) || + (next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb)); + if (!relativeBranch && (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0)) throw Domain::RelinkerException("Short guest TLS instruction is followed by an instruction that cannot move", header.Offset + offset + length); - moved.push_back({CheckedRva(rva + length), std::vector(bytes + length, bytes + length + next.Length), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt}); + std::vector movedBytes(bytes + length, bytes + length + next.Length); + std::optional branchTarget; + std::size_t branchDisplacementOffset = 0; + if (relativeBranch) { + const auto target = static_cast(rva) + static_cast(length + next.Length) + next.BranchDisp; + if (target < 0 || target > std::numeric_limits::max()) + throw Domain::RelinkerException("Short guest TLS instruction is followed by a branch beyond the address space", header.Offset + offset + length); + branchTarget = static_cast(target); + if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) { + movedBytes = next.Opcode == 0xeb ? std::vector{0xe9} : std::vector{0x0f, static_cast(0x80 | (next.Opcode & 0xf))}; + branchDisplacementOffset = movedBytes.size(); + movedBytes.insert(movedBytes.end(), 4, 0); + } else { + branchDisplacementOffset = next.Length - 4; + } + } + moved.push_back({CheckedRva(rva + length), std::move(movedBytes), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt, branchTarget, branchDisplacementOffset}); length += next.Length; } accesses.push_back({rva, header.Offset + offset, length, false, 0, addressRegister, 0, isAlu ? bytes[position] : std::uint8_t{0}, *address, wide, std::move(moved)}); @@ -357,6 +378,12 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Moved RIP-relative instruction exceeds rel32 range from its TLS stub", access.FileOffset); Io::WriteU32(bytes, *instruction.RipDisplacement, static_cast(static_cast(displacement))); } + if (instruction.BranchTarget) { + const auto displacement = static_cast(*instruction.BranchTarget) - (static_cast(code.GetRva()) + static_cast(bytes.size())); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Moved branch exceeds rel32 range from its TLS stub", access.FileOffset); + Io::WriteU32(bytes, instruction.BranchDisplacementOffset, static_cast(static_cast(displacement))); + } for (const auto byte : bytes) code.Emit({byte}); } diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 5abf3e195..8061ee163 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -511,7 +511,7 @@ def add_alias(image, size, begin=0x1200): def main(): groups = ("metadata", "padding", "register_loads", "displacement_bounds", "alu_execution", - "register_address_loads", "rejected") + "register_address_loads", "moved_branch", "rejected") groups += tuple(f"displacement_r{register}" for register in range(16) if register != 4) groups += tuple(f"alu_{name}" for name in ALU_READ) parser = argparse.ArgumentParser(description="Run Windows guest TLS function coverage fixtures") @@ -699,12 +699,35 @@ def main(): stub = pe_bytes_at(pe, 0x10000 + address + 5 + struct.unpack_from("= 0, (name, stub.hex()) + displacement = struct.unpack_from(" Date: Mon, 12 Oct 2026 07:59:34 +0400 Subject: [PATCH 221/318] perf(libkernel): commit Windows direct backing when mapped --- .../libs/prx/libc/include/WindowsMappings.hpp | 3 ++ .../libkernel/DirectMemory/DirectMemory.cpp | 2 +- core/libs/tests/GuestMemory.cpp | 29 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 4 +++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 9d33b9b0b..acb82476a 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -82,6 +82,9 @@ public: split(cursor + done, pageBytes); void* page = reinterpret_cast(cursor + done); if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(page, &memory, sizeof(memory)) != sizeof(memory)) fail("query shared guest page"); + if (memory.State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); DWORD previous; if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); const auto key = std::make_pair(reinterpret_cast(section), offset + done); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 9eeb24861..8f314928a 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -270,7 +270,7 @@ public: #ifdef _WIN32 static_cast(start); const auto size = static_cast(bytes); - section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, static_cast(size >> 32), static_cast(size), nullptr); + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(size >> 32), static_cast(size), nullptr); if (!section) { const auto error = static_cast(GetLastError()); char message[96]; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 74beb7554..574fa0106 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -289,6 +289,34 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelReleaseDirectMemory(again, page * 2) == 0); } +static void CheckSparseDirectMemoryBacking() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t size = 256u * 1024u * 1024u; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, size, 0, 0, &physical) == 0); + void* first = nullptr; + void* last = nullptr; + Require(sceKernelMapDirectMemory(&first, page, 3, 0, physical, 0) == 0); + Require(sceKernelMapDirectMemory(&last, page, 3, 0, physical + size - page, 0) == 0); + auto* firstBytes = static_cast(first); + auto* lastBytes = static_cast(last); + Require(firstBytes[0] == 0 && firstBytes[page - 1] == 0 && lastBytes[0] == 0 && lastBytes[page - 1] == 0); + firstBytes[0] = 17; + lastBytes[page - 1] = 29; + void* alias = nullptr; + Require(sceKernelMapDirectMemory(&alias, page, 3, 0, physical + size - page, 0) == 0); + auto* aliasBytes = static_cast(alias); + Require(aliasBytes[page - 1] == 29 && aliasBytes[0] == 0); + aliasBytes[page - 1] = 41; + Require(lastBytes[page - 1] == 41 && firstBytes[0] == 17); + Require(sceKernelMunmap(last, page) == 0 && sceKernelMunmap(alias, page) == 0); + last = nullptr; + Require(sceKernelMapDirectMemory(&last, page, 3, 0, physical + size - page, 0) == 0); + Require(static_cast(last)[page - 1] == 41); + Require(sceKernelMunmap(first, page) == 0 && sceKernelMunmap(last, page) == 0); + Require(sceKernelReleaseDirectMemory(physical, size) == 0); +} + static void CheckReleaseDirectMemoryClearsMappings() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -1715,6 +1743,7 @@ int main() { CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); CheckDirectMemoryFollowsPhysicalPages(); + CheckSparseDirectMemoryBacking(); CheckReleaseDirectMemoryClearsMappings(); CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 51f08cc9e..81ebe8628 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -9,6 +9,10 @@ - libwinpthread-1.dll - winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) are locked once when created. The host `std::shared_mutex` objects in [Recompiler.cpp](../../core/shader/recompiler/Recompiler.cpp) and [GuestWriteWatch.cpp](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) are not, and can still race on their first contended use. +### Direct memory + +- Windows direct-memory backing reserves a paging-file section and commits each mapped guest page before setting its protection. Committed physical contents remain in the section across unmap/remap and are released when the backing and its remaining views are destroyed; commitment is not recovered merely by unmapping a page. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. + ### Silent stubs Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: From 1e429dc23f527b823a4684aee289ef3858b6b063 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 12 Oct 2026 06:10:09 +0200 Subject: [PATCH 222/318] test(libs): cover watched recvfrom sender outputs --- core/libs/tests/GuestSocketHostWrite.cpp | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index f34c742a1..9624ce806 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -192,6 +192,55 @@ int main(int argc, char** argv) { "receive releases the host write guard"); } } + if (from && !stream) { + std::int64_t outputPhysical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &outputPhysical) == 0, + "allocate sender output memory"); + void* outputMapping = nullptr; + Require(sceKernelMapDirectMemory(&outputMapping, page * 2, 3, 0, outputPhysical, 0) == 0, + "map sender output memory"); + const auto collectOutputs = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(outputMapping), page * 2, + pages.data(), &count, true), "collect sender output writes"); + return count; + }; + for (const int mask : {1, 2, 3}) { + std::array hostAddress{}; + std::uint32_t hostLength = hostAddress.size(); + auto* address = (mask & 1) ? static_cast(outputMapping) + 16 : hostAddress.data(); + auto* length = (mask & 2) ? reinterpret_cast(static_cast(outputMapping) + page + 16) : + &hostLength; + { + const GuestArena::HostWrite addressDestination(address, hostAddress.size()); + const GuestArena::HostWrite lengthDestination(length, sizeof(*length)); + Require(addressDestination.Open() && lengthDestination.Open(), "initialize sender outputs"); + std::memset(address, 0xa5, hostAddress.size()); + *length = hostAddress.size(); + } + collectOutputs(); + Require(collectOutputs() == 0, "sender outputs initially clean"); + if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address is watched and armed"); + if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length is watched and armed"); + send(); + waitForPacket(); + reset(); + collect(); + *error = 123; + Require(receive(buffer, capacity, 0, address, length) == sizeof(payload) && *error == 123 && + std::memcmp(buffer, payload, sizeof(payload)) == 0, "receive into watched sender outputs"); + Require(*length == senderAddress.size() && std::memcmp(address, senderAddress.data(), *length) == 0, + "watched sender outputs are correct"); + const auto expectedPages = static_cast(((mask & 1) != 0) + ((mask & 2) != 0)) * (page / 4096); + Require(collectOutputs() == expectedPages, "each watched sender output is tracked"); + Require(collectOutputs() == 0, "sender output writes cleared"); + if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed"); + if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length rearmed"); + } + Require(sceKernelMunmap(outputMapping, page * 2) == 0, "unmap sender outputs"); + Require(sceKernelReleaseDirectMemory(outputPhysical, page * 2) == 0, "release sender output memory"); + } reset(); collect(); *error = 0; From 768929bb76f847ba28285f20b5b49688fb2d286d Mon Sep 17 00:00:00 2001 From: danzaio Date: Sat, 10 Oct 2026 13:41:45 -0300 Subject: [PATCH 223/318] test(agc): preparation failures name the shader address and stage FailureCapture now requires the code address and stage in the diagnosed failure, with and without the request dump. Fails before the wrap, passes after. AI-assisted: yes. --- .../tests/execution/PreparedShaders.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index ed91e5570..e34258828 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -200,7 +200,11 @@ void FailureCapture(AgcDriver::VulkanDevice& device) { #else Require(unsetenv("APS5_DUMP_SHADERS") == 0, "cannot disable shader capture"); #endif - Require(failure([&] { static_cast(AgcDriver::DriverDetail::PrepareShaderWithDiagnostics(request)); }) == expected, "disabled shader capture changed the preparation failure"); + std::ostringstream prefix; + prefix << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage) << ": "; + const auto diagnosed = failure([&] { static_cast(AgcDriver::DriverDetail::PrepareShaderWithDiagnostics(request)); }); + Require(diagnosed.rfind(prefix.str(), 0) == 0, "diagnosed failure names no shader address and stage"); + Require(diagnosed.substr(prefix.str().size()) == expected, "diagnosed failure changed the reason"); Require(!std::ifstream(path, std::ios::binary).is_open(), "disabled shader capture created a request"); #ifdef _WIN32 Require(_putenv_s("APS5_DUMP_SHADERS", "1") == 0, "cannot enable shader capture"); @@ -208,12 +212,14 @@ void FailureCapture(AgcDriver::VulkanDevice& device) { Require(setenv("APS5_DUMP_SHADERS", "1", 1) == 0, "cannot enable shader capture"); #endif const auto actual = failure([&] { static_cast(AgcDriver::DriverDetail::PrepareShaderWithDiagnostics(request)); }); + std::ostringstream dumpedPrefix; + dumpedPrefix << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage) << " (" << path << "): "; + Require(actual == dumpedPrefix.str() + expected, "enabled shader capture changed the diagnosed failure"); #ifdef _WIN32 Require(_putenv_s("APS5_DUMP_SHADERS", saved.c_str()) == 0, "cannot restore shader capture"); #else Require((previous != nullptr ? setenv("APS5_DUMP_SHADERS", saved.c_str(), 1) : unsetenv("APS5_DUMP_SHADERS")) == 0, "cannot restore shader capture"); #endif - Require(actual == expected, "shader capture changed the preparation failure"); std::ifstream file(path, std::ios::binary); std::ostringstream text; text << file.rdbuf(); From e71e3a4b3faefd0642206ec1f95c61fa67eac1d1 Mon Sep 17 00:00:00 2001 From: danzaio Date: Sun, 11 Oct 2026 23:51:39 -0300 Subject: [PATCH 224/318] fix(agc): name the shader address and stage in preparation failures PrepareShaderWithDiagnostics dumped the request but rethrew the raw error, so a failure never said which shader it came from. Wrap it with the shader address and stage, keeping the dump. AI-assisted: yes. --- .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 580d119af..457ce1b55 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include "RdnaDecoder/RdnaInstructionDecoder.hpp" @@ -31,6 +32,16 @@ namespace AgcDriver::DriverDetail { std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) { try { return ShaderRecompiler::PrepareShader(request, preparation); + } catch (const std::exception& error) { + std::string dump; + try { + if (std::getenv("APS5_DUMP_SHADERS") != nullptr) dump = Driver::dumpRequest(request.shader.codeAddress, request); + } catch (...) {} + std::ostringstream where; + where << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage); + if (!dump.empty()) where << " (" << dump << ")"; + where << ": "; + throw std::runtime_error(where.str() + error.what()); } catch (...) { try { if (std::getenv("APS5_DUMP_SHADERS") != nullptr) static_cast(Driver::dumpRequest(request.shader.codeAddress, request)); From 56df893667a484c8e2e4057871f55804fca30f9f Mon Sep 17 00:00:00 2001 From: danzaio Date: Mon, 12 Oct 2026 00:46:50 -0300 Subject: [PATCH 225/318] fix(agc): preserve dumps for non-standard shader failures Capture the request once before rethrowing from the shared handler. Standard exceptions keep the shader address, stage, optional dump filename and full message; non-standard exceptions remain unchanged after capture. AI-assisted: yes. --- .../src/Driver/Shaders/ShaderRegistry.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 457ce1b55..e31f423ba 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -30,23 +31,24 @@ namespace AgcDriver::DriverDetail { std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) { + std::exception_ptr failure; + std::string dump; try { return ShaderRecompiler::PrepareShader(request, preparation); - } catch (const std::exception& error) { - std::string dump; + } catch (...) { + failure = std::current_exception(); try { if (std::getenv("APS5_DUMP_SHADERS") != nullptr) dump = Driver::dumpRequest(request.shader.codeAddress, request); } catch (...) {} + } + try { + std::rethrow_exception(failure); + } catch (const std::exception& error) { std::ostringstream where; where << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage); if (!dump.empty()) where << " (" << dump << ")"; where << ": "; throw std::runtime_error(where.str() + error.what()); - } catch (...) { - try { - if (std::getenv("APS5_DUMP_SHADERS") != nullptr) static_cast(Driver::dumpRequest(request.shader.codeAddress, request)); - } catch (...) {} - throw; } } From 2441a4b0d9f472fe38bc44634a9aa620f2f29663 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Mon, 12 Oct 2026 07:34:15 +0300 Subject: [PATCH 226/318] fix(agc): retry failed host imports after range replacement Retain weak identities and the full failed span so a replacement of either owner or a shorter remaining run retries the import. Keep unchanged failures cached across unrelated mapping changes without pinning their allocations. Existing upstream import lifetime, span windows and Windows fallback remain in place. AI-assisted implementation. --- .../Graphics/src/GuestBufferMemory.cpp | 27 ++++++--- .../libSceAgcDriver/tests/BdaResources.cpp | 57 +++++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 73e1015e5..832d44eeb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -142,6 +142,12 @@ bool addressSpaceCacheEnabled() { return !disabled; } +struct ImportFailure { + std::uint64_t base; + std::uint64_t bytes; + GuestAllocations::Mapped ranges; +}; + // Imports persist across draws, keyed by allocation base, and are dropped when their allocation leaves // the registered set; a dropped import is destroyed once the recorded work that may read it completed. struct HostImports { @@ -152,7 +158,7 @@ struct HostImports { std::map imports; std::map>> parked; std::uint64_t liveBytes = 0; - std::set failed; + std::map failed; std::set unaliasedSpans; // Registry generation the imports were last reconciled with. std::uint64_t refreshedGeneration = 0; @@ -595,7 +601,8 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u return !run.ranges.empty() && end <= run.base + run.bytes; } -bool sameRun(const HostImport& entry, const ImportRun& run) { +template +bool sameRun(const Entry& entry, const ImportRun& run) { if (run.base != entry.base || run.bytes != entry.bytes || run.ranges.size() != entry.ranges.size()) return false; for (std::size_t index = 0; index < run.ranges.size(); ++index) { if (entry.ranges[index].owner_before(run.ranges[index]) || run.ranges[index].owner_before(entry.ranges[index])) return false; @@ -629,7 +636,11 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s } if (auto it = state.imports.lower_bound(base); it != state.imports.begin() && std::prev(it)->second.base + std::prev(it)->second.bytes > base) retireImport(context.device, state, std::prev(it), lease); const auto alignment = context.hostImportAlignment; - if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0 || state.failed.contains(base)) return nullptr; + if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; + if (const auto failed = state.failed.find(base); failed != state.failed.end()) { + if (sameRun(failed->second, run)) return nullptr; + state.failed.erase(failed); + } // Pinned imports count against the driver's system memory budget; past it ordinary host // allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the // registered memory of address-based shaders; past it they copy gigabytes per dispatch). @@ -661,7 +672,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s for (std::uint64_t cursor = base; cursor < base + bytes;) { MEMORY_BASIC_INFORMATION info{}; if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) { - state.failed.insert(base); + state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } const auto protection = info.Protect & 0xffu; @@ -673,7 +684,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; } if (!writable && readOnly) { - state.failed.insert(base); + state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } if (!writable) { @@ -692,7 +703,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s } #else if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) { - state.failed.insert(base); + state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } #endif @@ -712,7 +723,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s #ifdef _WIN32 GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); #endif - state.failed.insert(base); + state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast(base), static_cast(bytes), step, static_cast(result)); #ifdef _WIN32 static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; @@ -800,7 +811,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc retireImport(context.device, state, it, lease); it = next; } - for (auto it = state.failed.begin(); it != state.failed.end();) it = leasedRangeAt(lease, *it) != nullptr ? std::next(it) : state.failed.erase(it); + for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRun(it->second, importRun(lease, it->first)) ? std::next(it) : state.failed.erase(it); } const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 777c3fa77..392eab435 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -119,6 +119,62 @@ void importSpanTests(const Context& context) { ::operator delete(block, std::align_val_t{piece}); } +void failedImportRemapTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + auto* block = static_cast(::operator new(4 * piece, std::align_val_t{piece})); + auto importing = context; + importing.hostImportAlignment = piece; + const auto address = reinterpret_cast(block); + const auto add = [&](std::size_t index) { + GuestAllocations::Mutation mutation; + mutation.Add(block + index * piece, piece, true, true, true); + }; + const auto remove = [&](std::size_t index) { + GuestAllocations::Mutation mutation; + mutation.Remove(block + index * piece); + }; + SetHostImportWindow(0); + for (std::size_t replaced = 0; replaced < 2; ++replaced) { + add(0); + add(1); + std::weak_ptr owner; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto found = std::find_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == address + replaced * piece; }); + Require(found != lease.end(), "failed import fixture has no registered owner"); + owner = *found; + } + access.limitMemory(0); + const auto attempts = access.allocationAttempts(); + Require(HostImportFor(importing, address, 2 * piece) == nullptr && access.allocationAttempts() == attempts + 1, "host import failure was not exercised"); + Require(owner.use_count() == 1, "a failed import pins its owner"); + Require(HostImportFor(importing, address, 2 * piece) == nullptr && access.allocationAttempts() == attempts + 1, "an unchanged failed import was retried"); + add(3); + Require(HostImportFor(importing, address, 2 * piece) == nullptr && access.allocationAttempts() == attempts + 1, "an unrelated mapping retried a failed import"); + remove(replaced); + Require(owner.expired(), "a failed import kept the replaced owner alive"); + add(replaced); + access.limitMemory(std::nullopt); + const auto* imported = HostImportFor(importing, address, 2 * piece); + Require(imported != nullptr && imported->base == address && imported->bytes == 2 * piece && access.allocationAttempts() == attempts + 2, "same-address remap retained a failed import decision"); + remove(0); + remove(1); + remove(3); + ClearHostImports(importing.device); + } + add(0); + add(1); + access.limitMemory(0); + Require(HostImportFor(importing, address, 2 * piece) == nullptr, "span failure was not exercised"); + remove(1); + access.limitMemory(std::nullopt); + const auto* shortened = HostImportFor(importing, address, piece); + Require(shortened != nullptr && shortened->bytes == piece, "a failed span suppressed an import of its remaining range"); + remove(0); + ClearHostImports(importing.device); + ::operator delete(block, std::align_val_t{piece}); +} + void importWindowTests(const Context& context) { constexpr std::size_t piece = 65536; constexpr std::size_t window = 2 * piece; @@ -1105,6 +1161,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { } importCrossingTests(context, access); importSpanTests(context); + failedImportRemapTests(context, access); importWindowTests(context); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); From 9db923e907d99a28cc4feb95a6e7984751966e58 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:05:44 +0200 Subject: [PATCH 227/318] fix(shader): clip primitives with an all-zero clip position --- .../Execution/src/ShaderDeviceProfile.cpp | 1 + .../Execution/src/VulkanDevice.cpp | 4 + .../Graphics/include/Context.hpp | 1 + .../Graphics/include/Pipeline.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- .../Graphics/src/ShaderValidation.cpp | 13 +- .../tests/PositionInvariance.cpp | 3 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 8 +- .../tests/SpirvNarrowConstantStore.cpp | 4 +- .../libSceAgcDriver/tests/VertexBarrier.cpp | 3 + .../tests/execution/ZeroPositionVertex.cpp | 137 ++++++++++++++++++ .../SpirvBackend/SpirvEmitterState.hpp | 2 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 17 +++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 29 ++++ docs/dev/TechnicalDebt.md | 1 + 15 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 3d3794279..0e55dd5ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -47,6 +47,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta checkFeature(spv::CapabilityFloat64, core.shaderFloat64 == VK_TRUE, "shaderFloat64"); checkFeature(spv::CapabilityTessellation, core.tessellationShader == VK_TRUE, "tessellationShader"); checkFeature(spv::CapabilityImageGatherExtended, core.shaderImageGatherExtended == VK_TRUE, "shaderImageGatherExtended"); + checkFeature(spv::CapabilityClipDistance, core.shaderClipDistance == VK_TRUE, "shaderClipDistance"); checkFeature(spv::CapabilityMinLod, core.shaderResourceMinLod == VK_TRUE, "shaderResourceMinLod"); checkFeature(spv::CapabilityStorageImageMultisample, core.shaderStorageImageMultisample == VK_TRUE, "shaderStorageImageMultisample"); checkFeature(spv::CapabilityStorageImageReadWithoutFormat, core.shaderStorageImageReadWithoutFormat == VK_TRUE, "shaderStorageImageReadWithoutFormat"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 8a1600be3..fa9d8b151 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -262,6 +262,7 @@ struct VulkanDevice::State { bool fragmentShaderBarycentric = false; bool geometryShader = false; bool sampleRateShading = false; + bool shaderClipDistance = false; bool shaderClock = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; @@ -1348,6 +1349,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->sampleRateShading = enabled.sampleRateShading == VK_TRUE; if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry); enabled.shaderClipDistance = available.shaderClipDistance; + state->shaderClipDistance = enabled.shaderClipDistance == VK_TRUE; + if (enabled.shaderClipDistance) state->capabilities.push_back(spv::CapabilityClipDistance); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); state->storageImageWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE && enabled.shaderStorageImageReadWithoutFormat == VK_TRUE; @@ -3012,6 +3015,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageInt64Atomics = state->imageInt64Atomics; context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; + context.shaderClipDistance = state->shaderClipDistance; context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 38049d9e6..d1e641ab6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -158,6 +158,7 @@ struct Context { bool imageInt64Atomics = false; bool geometryShader = false; bool sampleRateShading = false; + bool shaderClipDistance = false; bool nullDescriptors = false; bool bufferInt64Atomics = false; const PlaceholderImages* placeholderImages = nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index bb817d437..49fb7769c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -110,7 +110,7 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); // Returns the color attachment locations the pixel shader writes. -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false); +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 09fc3ffbc..a25b9ec2f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -732,7 +732,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< } std::set outputs; try { - outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index c5d9168d1..d30076237 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -43,6 +43,7 @@ struct Module { std::map outputs; std::optional secondSource; bool position = false; + bool clipDistance = false; bool primitiveIndices = false; bool fragmentBarycentric = false; std::map> modes; @@ -109,6 +110,11 @@ struct Module { if (!input) position = true; return; } + if (value == spv::BuiltInClipDistance && !input && (vertex || stage == Stage::TessellationEvaluation)) { + Require(!clipDistance && Signature(type) == "f32[1]", "invalid or duplicate clip distance output"); + clipDistance = true; + return; + } if (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation || stage == Stage::Mesh) { const auto signature = Signature(type); if (!input && value == spv::BuiltInPosition) { @@ -136,7 +142,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -232,6 +238,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImage1D || capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || + ((vertex || evaluation) && shaderClipDistance && capability == spv::CapabilityClipDistance) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || @@ -547,7 +554,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics) { +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -568,7 +575,7 @@ std::set ValidateShaders(std::span shaders, Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); diff --git a/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp b/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp index 2c9b168bb..5b6d1b3be 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp @@ -36,6 +36,8 @@ void requireInvariantPositions(const ShaderRecompiler::RecompileResult& result, constexpr std::array PositionExport{0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array VertexCapabilities{spv::CapabilityClipDistance}; + ShaderRecompiler::RecompileRequest baseRequest(ShaderRecompiler::ShaderStage stage) { using namespace ShaderRecompiler; RecompileRequest request{}; @@ -45,6 +47,7 @@ ShaderRecompiler::RecompileRequest baseRequest(ShaderRecompiler::ShaderStage sta request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; + request.target.supportedCapabilities = VertexCapabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return request; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2a86162fa..1526845de 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -50,7 +50,7 @@ namespace { ShaderRecompiler::SpirvTarget BufferTarget() { - static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; ShaderRecompiler::SpirvTarget target{}; target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; @@ -1202,7 +1202,7 @@ void verifyLanesOutsideHostSubgroup() { void verifyHalfWaveReduction() { using namespace ShaderRecompiler; - static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; const auto pixel = [](std::span code, std::uint32_t subgroupSize, bool arithmetic) { ShaderPixelStageInfo info{}; info.inputAddr = PixelInputBit(PixelInput::PositionX); @@ -1364,7 +1364,7 @@ void verifyHalfWaveReduction() { void verifyNarrowVertexWave() { using namespace ShaderRecompiler; - static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; static constexpr std::array userData{0u, 0u, 0u, 0x2020u}; const auto vertex = [](std::span code, std::uint32_t waveSize, std::uint32_t subgroupSize, std::uint32_t userDataBase) { RecompileRequest request{}; @@ -1411,7 +1411,7 @@ void verifyNarrowVertexWave() { const auto wide64 = prepared(culling, 64u); require(!wide64.empty() && reductions(wide64) == 0u, "narrow vertex wave: the NGG culling scan, keyed on lanes below 64 on 64 lanes, did not keep its DPP scan"); require(prepares(patched({{6, 0x80060015u}}), 32u), "narrow vertex wave: the scan keyed on lanes below the vertex count in s3 did not prepare on 32 lanes"); - static constexpr std::array noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}; + static constexpr std::array noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityClipDistance}; const auto otherKeys = patched({{7, 0x360e0a82u}}); auto withoutArithmetic = vertex(otherKeys, 64u, 32u, 8u); withoutArithmetic.target.supportedCapabilities = noArithmetic; diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp index daa5ac849..ae99defb9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp @@ -50,8 +50,8 @@ void CheckStorageOnlyModule(const RecompileResult& result, std::string_view name } SpirvTarget StorageOnlyTarget() { - static constexpr std::array capabilities{ - spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, }; static constexpr std::array extensions{ "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp index 8d591fe05..453fbbab7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp @@ -12,6 +12,8 @@ namespace { constexpr std::array BarrierExport{0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; constexpr std::array BarrierLdsExport{0xd8340000u, 0u, 0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array VertexCapabilities{spv::CapabilityClipDistance}; + ShaderRecompiler::RecompileRequest vertexRequest(std::span code) { using namespace ShaderRecompiler; RecompileRequest request{}; @@ -21,6 +23,7 @@ ShaderRecompiler::RecompileRequest vertexRequest(std::span request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 32; + request.target.supportedCapabilities = VertexCapabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return request; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp new file mode 100644 index 000000000..b5c7ad5c5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp @@ -0,0 +1,137 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Extent = 64u; +alignas(256) std::array pixels{}; +alignas(256) std::array vertices{}; +constexpr std::array vertexCode{0xf4080100u, 0xfa000000u, 0x4a0a0a02u, 0x4a0a0b08u, 0x4a0a0a03u, 0xe00c2000u, 0x80010005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +constexpr std::array pixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; + +using Position = std::array; + +struct Counts { + unsigned total = 0; + unsigned upper = 0; + std::array, 16> map{}; +}; + +Counts RunCase(AgcDriver::VulkanDevice& device, const char* name, std::uint32_t records, const std::vector& positions, std::size_t stored, VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { + vertices.fill(std::byte{0}); + constexpr std::uint32_t stride = 16u; + for (std::size_t index = 0; index < stored; ++index) std::memcpy(vertices.data() + index * stride, positions[index].data(), 16u); + std::array descriptor{}; + const auto tableAddress = reinterpret_cast(descriptor.data()); + std::array userData{static_cast(tableAddress), static_cast(tableAddress >> 32u), 0u, 0u}; + const std::array memory{{{tableAddress, std::as_bytes(std::span(descriptor))}}}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.fetchEmbedded = true; + vertexInfo.resourcesNum = 1u; + vertexInfo.resourcesDst[0] = {0, 4, 0, 0}; + ShaderRecompiler::RecompileRequest request{{ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {32u, 0u, userData, std::nullopt, std::nullopt, vertexInfo, memory}, device.Target(), {0u, 0u, 0u, 96u}}; + const auto address = reinterpret_cast(vertices.data()); + descriptor = {static_cast(address), static_cast(address >> 32u) | (stride << 16u), records, 0x01000facu | (77u << 12u)}; + request.context.vertex->resources[0].fields = descriptor; + const auto vertex = ShaderRecompiler::Recompile(request); + const auto pushBytes = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.targetOutputMode[0] = 9u; + pixelInfo.targetExportMapping.fill(0xe4u); + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}}; + const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Extent, Extent}; + state.topology = topology; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + AgcDriver::Pm4::DrawParameters draw{0u, static_cast(positions.size()), 0u, 1u, 0u, false}; + draw.firstInstance = 0u; + pixels.fill(std::byte{0}); + device.Draw(state, draw, shaders); + device.WaitIdle(); + Counts counts; + for (std::uint32_t y = 0; y < Extent; ++y) { + for (std::uint32_t x = 0; x < Extent; ++x) { + const auto index = (y * Extent + x) * 4u; + if (pixels[index] == std::byte{0} && pixels[index + 3u] == std::byte{0}) continue; + ++counts.total; + const float ndcX = (static_cast(x) + 0.5f) / Extent * 2.0f - 1.0f; + const float ndcY = 1.0f - (static_cast(y) + 0.5f) / Extent * 2.0f; + if (ndcX + ndcY > -1.0f) ++counts.upper; + ++counts.map[y / 4u][x / 4u]; + } + } + std::cout << "case " << name << ": records=" << records << " total=" << counts.total << " outsideFirstTriangle(x+y>-1)=" << counts.upper << "\n"; + for (const auto& row : counts.map) { + std::string line; + for (const auto count : row) line += count == 0u ? '.' : count == 16u ? '#' : '+'; + std::cout << " " << line << "\n"; + } + return counts; +} + +void Run(AgcDriver::VulkanDevice& device) { + std::cout << "device: " << device.DeviceName() << "\n"; + const Position v0{-1.0f, -1.0f, 0.0f, 1.0f}; + const Position v1{0.0f, -1.0f, 0.0f, 1.0f}; + const Position v2{-1.0f, 0.0f, 0.0f, 1.0f}; + const auto make = [&](const Position& last) { return std::vector{v0, v1, v2, last}; }; + const auto a = RunCase(device, "A out-of-range vertex 3", 3u, make({0.0f, 0.0f, 0.0f, 0.0f}), 3u); + const auto b = RunCase(device, "B in-range (0,0,0,0)", 4u, make({0.0f, 0.0f, 0.0f, 0.0f}), 4u); + const auto c = RunCase(device, "C repeat of v1", 4u, make(v1), 4u); + const auto d = RunCase(device, "D (0,0,0,1) at the centre", 4u, make({0.0f, 0.0f, 0.0f, 1.0f}), 4u); + const auto e = RunCase(device, "E (1,1,0,1) real triangle", 4u, make({1.0f, 1.0f, 0.0f, 1.0f}), 4u); + const Position zero{0.0f, 0.0f, 0.0f, 0.0f}; + const auto f = RunCase(device, "F negative zeros", 4u, make({-0.0f, 0.0f, -0.0f, -0.0f}), 4u); + const auto g = RunCase(device, "G triangle list, second triangle has (0,0,0,0)", 6u, {v0, v1, v2, v1, v2, zero}, 6u, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + const auto h = RunCase(device, "H strip with two (0,0,0,0) vertices", 5u, {v0, v1, v2, zero, zero}, 5u); + const auto p = RunCase(device, "P point list, (-0.5,-0.5,0,1) and (0,0,0,0)", 2u, {{-0.5f, -0.5f, 0.0f, 1.0f}, zero}, 2u, VK_PRIMITIVE_TOPOLOGY_POINT_LIST); + Require(e.upper > 0u, "probe cannot see the second triangle"); + Require(c.total > 0u && c.upper == 0u, "the first triangle is not drawn alone"); + Require(a.upper == 0u && a.total == c.total, "a vertex fetched past num_records must clip the primitive away"); + Require(b.upper == 0u && b.total == c.total, "a stored (0,0,0,0) vertex must clip the primitive away"); + Require(f.upper == 0u && f.total == c.total, "a stored (-0,0,-0,-0) vertex must clip the primitive away"); + Require(g.upper == 0u && g.total == c.total, "a triangle list with a (0,0,0,0) vertex must keep only the first triangle"); + Require(h.upper == 0u && h.total == c.total, "a strip with two (0,0,0,0) vertices must keep only the first triangle"); + Require(d.upper == 496u, "a (0,0,0,1) vertex must not be clipped away"); + Require(p.total == 1u, "a point at (0,0,0,0) must not be drawn"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index a876a4062..ca69aa93c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -164,6 +165,7 @@ struct SpirvEmitterState { std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; std::uint32_t clipDistanceVariable = 0; + std::optional zeroPositionClipIndex; std::uint32_t cullDistanceVariable = 0; std::uint32_t layerVariable = 0; std::uint32_t viewportIndexVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 3cd67e86d..c484fa2b8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -291,6 +291,8 @@ void DefineInputs(SpirvEmitterState& state) { } } +constexpr std::uint32_t MaxClipDistances = 8u; + void DefineOutputs(SpirvEmitterState& state) { state.outputs.reserve(state.program.Info().outputs.size()); std::uint32_t clipDistanceCount = 0u; @@ -307,6 +309,18 @@ void DefineOutputs(SpirvEmitterState& state) { DefineMeshOutputs(state); return; } + const auto stage = state.program.Resources().stage; + const bool exportsPosition = std::ranges::any_of(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; }); + if (exportsPosition && (stage == IrShaderStage::Vertex || stage == IrShaderStage::TessellationEvaluation)) { + if (std::ranges::find(state.supportedCapabilities, spv::CapabilityClipDistance) == state.supportedCapabilities.end()) { + FailEmit("clipping a primitive with an all-zero clip position needs shaderClipDistance on the target device"); + } + state.zeroPositionClipIndex = clipDistanceCount; + clipDistanceCount += 1u; + if (clipDistanceCount > MaxClipDistances) { + FailEmit("guest clip distances plus the all-zero clip position distance exceed " + std::to_string(MaxClipDistances) + " clip distances"); + } + } const auto BuiltIn = [&](std::uint32_t& variable, std::uint32_t type, const char* name, std::uint32_t builtin) { if (variable == 0u) { variable = DefineInterfaceVariable(state, type, spv::StorageClassOutput, name); @@ -355,6 +369,9 @@ void DefineOutputs(SpirvEmitterState& state) { } } } + if (state.zeroPositionClipIndex) { + BuiltIn(state.clipDistanceVariable, F32ArrayType(state, clipDistanceCount), "gl_ClipDistance", spv::BuiltInClipDistance); + } if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage) { if (state.inputInfo.pixel->psSampleMaskExportEnable) throw std::runtime_error("alpha-to-coverage with a sample-mask export is unsupported"); BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 8a507d409..1793ea2f8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -417,6 +417,32 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const } } +void StoreZeroPositionClipDistance(SpirvEmitterState& state, std::uint32_t position) { + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeU32Vector(state, 4u), bits, position); + std::uint32_t combined = 0u; + for (std::uint32_t component = 0; component < 4u; component++) { + const auto raw = state.module.AllocateId(); + const auto magnitude = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), raw, bits, component); + state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), magnitude, raw, ConstantU32(state, 0x7fffffffu)); + if (component == 0u) { + combined = magnitude; + continue; + } + const auto merged = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitwiseOr, TypeU32(state), merged, combined, magnitude); + combined = merged; + } + const auto allZero = state.module.AllocateId(); + const auto distance = state.module.AllocateId(); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), allZero, combined, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpSelect, TypeF32(state), distance, allZero, ConstantF32Value(state, -1.0f), ConstantF32Value(state, 0.0f)); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, state.clipDistanceVariable, ConstantU32(state, *state.zeroPositionClipIndex)); + state.module.AddFunction(spv::OpStore, pointer, distance); +} + std::uint32_t ConvertClipCoordinate(SpirvEmitterState& state, std::uint32_t coordinate, float scale, float offset, float halfExtent) { const auto window = state.module.AllocateId(); const auto biased = state.module.AllocateId(); @@ -1312,6 +1338,9 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { value = ConvertPositionToClipSpace(state, value); } } + if (exp.kind == ExportTargetKind::Position && state.zeroPositionClipIndex) { + StoreZeroPositionClipDistance(state, value); + } if (state.program.Resources().stage == IrShaderStage::Mesh) { const auto kind = exp.kind == ExportTargetKind::Position ? StageOutputKind::Position : StageOutputKind::Parameter; state.module.AddFunction(spv::OpStore, MeshOutputPointer(state, kind, exp.index), value); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c1247e46b..f5709b025 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -625,6 +625,7 @@ Throughout the project, every function at every stage either **does exactly what - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. - The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) decode to their own operand kinds (scalar source codes 235 to 238, named as in LLVM's `SIRegisterInfo.td`), but their value is not modelled: reading one refuses with the source it refused named, because the wave's shared and private aperture bases and limits are unknown. Both the scalar-code read and the raw-operand read refuse; a shader that only carries the value into an address still prepares, one that reads it does not - [VertexBufferReadSize](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. +- [EmitSetAttribute](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated rect-list and barycentric stages do not write it, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. - [sceImeKeyboardGetResourceId](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard - [Player review dialog](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does - [NP reachability callbacks](../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. From f0e852ae277cb9dfefe29d73b10b93755cae67bc Mon Sep 17 00:00:00 2001 From: BharatYadav007 Date: Sun, 11 Oct 2026 17:41:38 +0530 Subject: [PATCH 228/318] fix(libc): normalize valid mktime minus-one results --- core/libs/prx/libc/CMakeLists.txt | 10 +++ core/libs/prx/libc/src/Time.cpp | 3 +- core/libs/tests/GuestMktime.cpp | 105 ++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestMktime.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index b3830d4cc..a30a20b83 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -68,6 +68,16 @@ if(BUILD_TESTING) set_tests_properties(guest_time_conversion PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_mktime_tests ${LIBS_INCLUDE_DIR}/tests/GuestMktime.cpp) + target_include_directories(guest_mktime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_mktime_tests PRIVATE libc) + configure_windows_unwind(guest_mktime_tests) + add_test(NAME guest_mktime COMMAND guest_mktime_tests) + set_tests_properties(guest_mktime PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_mktime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(precise_wait_tests ${LIBS_INCLUDE_DIR}/tests/PreciseWait.cpp) target_include_directories(precise_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(precise_wait_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index 1da075245..4c8e794c6 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -305,8 +305,9 @@ GuestTm* APS5_VABI gmtime_nid_postfix(const int64_t* timer) { int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr) { std::tm host = toHostTm(*timeptr); + host.tm_wday = -1; const std::time_t t = std::mktime(&host); - if (t == static_cast(-1)) return -1; + if (t == static_cast(-1) && host.tm_wday == -1) return -1; const int64_t time = static_cast(t); localtime_s_nid_postfix(&time, timeptr); return time; diff --git a/core/libs/tests/GuestMktime.cpp b/core/libs/tests/GuestMktime.cpp new file mode 100644 index 000000000..0a81573f9 --- /dev/null +++ b/core/libs/tests/GuestMktime.cpp @@ -0,0 +1,105 @@ +#include "prx/libc/include/GuestTime.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm*); +std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); +GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); +void APS5_VABI tzset_nid_postfix(); +} + +using Converter = std::int64_t (APS5_VABI *)(GuestTm*); + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static bool Equal(const GuestTm& left, const GuestTm& right) { + return left.tm_sec == right.tm_sec && left.tm_min == right.tm_min && left.tm_hour == right.tm_hour + && left.tm_mday == right.tm_mday && left.tm_mon == right.tm_mon && left.tm_year == right.tm_year + && left.tm_wday == right.tm_wday && left.tm_yday == right.tm_yday && left.tm_isdst == right.tm_isdst + && left.tm_gmtoff == right.tm_gmtoff && left.tm_zone != nullptr && right.tm_zone != nullptr + && std::strcmp(left.tm_zone, right.tm_zone) == 0; +} + +static void CheckNormalization(Converter convert, std::int64_t timer) { + GuestTm expected{}; + Require(localtime_s_nid_postfix(&timer, &expected) == &expected, "Local conversion failed"); + GuestTm input = expected; + input.tm_sec += 60; + --input.tm_min; + input.tm_wday = -1; + input.tm_yday = -1; + input.tm_isdst = -1; + input.tm_gmtoff = -1; + input.tm_zone = nullptr; + errno = EOVERFLOW; + Require(convert(&input) == timer, "mktime returned the wrong timestamp"); + Require(Equal(input, expected), "mktime did not normalize a valid timestamp"); +} + +static void CheckFailure(Converter convert) { + GuestTm input{}; + input.tm_year = std::numeric_limits::max(); + input.tm_mon = std::numeric_limits::max(); + input.tm_mday = 1; + input.tm_wday = 4; + input.tm_yday = 12; + input.tm_isdst = -1; + input.tm_gmtoff = 123; + input.tm_zone = "unchanged"; + const GuestTm original = input; + Require(convert(&input) == -1, "mktime accepted an unrepresentable calendar"); + Require(Equal(input, original), "Failed mktime changed the guest calendar"); +#ifdef _WIN32 + input = {}; + input.tm_year = 69; + input.tm_mon = 11; + input.tm_mday = 31; + input.tm_hour = 23; + input.tm_min = 59; + input.tm_sec = 59; + input.tm_zone = "unchanged"; + const GuestTm beforeEpoch = input; + Require(convert(&input) == -1, "Windows mktime accepted a time before the epoch"); + Require(Equal(input, beforeEpoch), "Failed Windows mktime changed the guest calendar"); +#endif +} + +static void CheckZone(const char* zone) { +#ifdef _WIN32 + Require(_putenv_s("TZ", zone) == 0, "Could not set the time zone"); +#else + Require(setenv("TZ", zone, 1) == 0, "Could not set the time zone"); +#endif + tzset_nid_postfix(); + constexpr std::array converters{libc_mktime_nid_postfix, mktime_nid_postfix}; + constexpr std::array timers{-86401, -2, -1, 0, 1, 59, 60, 86400}; + for (auto convert : converters) { + for (const auto timer : timers) { +#ifdef _WIN32 + if (timer < 0) continue; +#endif + CheckNormalization(convert, timer); + } + CheckFailure(convert); + } +} + +int main() { + CheckZone("UTC0"); + CheckZone("UTC-2"); +#ifndef _WIN32 + CheckZone("UTC5"); +#endif +} From 6197edea2b35f932c10f71a4ae5e087694aa9e24 Mon Sep 17 00:00:00 2001 From: Aarav <211792885+AaravMehta-07@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:21:17 +0530 Subject: [PATCH 229/318] fix(libkernel): report copied socket address lengths --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 +- core/libs/tests/GuestTcp.cpp | 26 +++++++++- core/libs/tests/GuestUdp.cpp | 55 ++++++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 0ac8f3163..9b7b9d868 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -171,8 +171,9 @@ void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* l std::memcpy(bytes + 8, &v6.sin6_addr, 16); std::memcpy(bytes + 24, &v6.sin6_scope_id, 4); } - std::memcpy(output, bytes, std::min(*length, bytes[0])); - *length = bytes[0]; + const auto copied = std::min(*length, bytes[0]); + std::memcpy(output, bytes, copied); + *length = copied; } } diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index c6fb00256..5afeaa98e 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -73,6 +74,29 @@ int main() { Require(getpeername_nid_postfix(accepted, connected_peer.data(), &address_size) == 0); Require(address_size == 16 && connected_peer[1] == 2); + for (auto query : {getsockname_nid_postfix, getpeername_nid_postfix}) { + std::array expected{}; + address_size = expected.size(); + Require(query(accepted, expected.data(), &address_size) == 0 && address_size == expected.size()); + for (std::uint32_t capacity : {0u, 1u, 2u, 15u, 16u, 28u, UINT32_MAX}) { + std::array actual; + actual.fill(0xa5); + address_size = capacity; + const auto copied = std::min(capacity, std::uint32_t{16}); + Require(query(accepted, actual.data(), &address_size) == 0 && address_size == copied); + Require(std::memcmp(actual.data(), expected.data(), copied) == 0); + Require(std::all_of(actual.begin() + copied, actual.end(), [](auto byte) { return byte == 0xa5; })); + } + } + const int shortClient = socket_nid_postfix(2, 1, 0); + Require(shortClient >= 0 && connect_nid_postfix(shortClient, address.data(), address.size()) == 0); + peer.fill(0xa5); + address_size = 1; + const int shortAccepted = accept_nid_postfix(listener, peer.data(), &address_size); + Require(shortAccepted >= 0 && address_size == 1 && peer[0] == 16); + Require(std::all_of(peer.begin() + 1, peer.end(), [](auto byte) { return byte == 0xa5; })); + Require(close_nid_postfix(shortAccepted) == 0 && close_nid_postfix(shortClient) == 0); + const char request[] = "guest TCP loopback"; char received[sizeof(request)]{}; Require(send_nid_postfix(client, request, sizeof(request), 0x1) == -1 && *__error_nid_postfix() == 45); @@ -91,4 +115,4 @@ int main() { Require(close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0); Require(close_nid_postfix(listener) == -1 && *__error_nid_postfix() == 9); -} \ No newline at end of file +} diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 2f69a2201..c8930b831 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -42,7 +43,61 @@ std::int64_t APS5_VABI sceKernelReadv(int, const Iovec*, int); std::int64_t APS5_VABI sceKernelWritev(int, const Iovec*, int); } static void Require(bool value) { if (!value) std::abort(); } + +static void CheckAddressLengths(int family) { + const std::uint32_t addressSize = family == 2 ? 16 : 28; + const int receiver = socket_nid_postfix(family, 2, 0); + if (receiver < 0 && family == 28 && *__error_nid_postfix() == 47) return; + Require(receiver >= 0); + const int sender = socket_nid_postfix(family, 2, 0); + Require(sender >= 0); + std::array destination{}; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const int bound = bind_nid_postfix(receiver, destination.data(), addressSize); + if (bound < 0 && family == 28 && *__error_nid_postfix() == 49) { + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); + return; + } + Require(bound == 0); + std::uint32_t size = destination.size(); + Require(getsockname_nid_postfix(receiver, destination.data(), &size) == 0 && size == addressSize); + Require(connect_nid_postfix(sender, destination.data(), addressSize) == 0); + std::array expected{}; + size = expected.size(); + Require(getsockname_nid_postfix(sender, expected.data(), &size) == 0 && size == addressSize); + for (std::uint32_t capacity : {0u, 1u, 2u, addressSize - 1, addressSize, 32u, UINT32_MAX}) { + const auto copied = std::min(capacity, addressSize); + std::array address; + address.fill(0xa5); + size = capacity; + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0 && size == copied); + Require(std::memcmp(address.data(), destination.data(), copied) == 0); + Require(std::all_of(address.begin() + copied, address.end(), [](auto byte) { return byte == 0xa5; })); + const char payload[] = "address length"; + char received[sizeof(payload)]{}; + Require(sendto_nid_postfix(sender, payload, sizeof(payload), 0, nullptr, 0) == sizeof(payload)); + address.fill(0xa5); + size = capacity; + Require(recvfrom_nid_postfix(receiver, received, sizeof(received), 2, address.data(), &size) == sizeof(payload)); + Require(size == copied && std::memcmp(address.data(), expected.data(), copied) == 0); + Require(std::all_of(address.begin() + copied, address.end(), [](auto byte) { return byte == 0xa5; })); + address.fill(0xa5); + Iovec vector{received, sizeof(received)}; + Msghdr message{address.data(), capacity, &vector, 1, nullptr, 0, -1}; + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(payload)); + Require(message.nameLength == copied && std::memcmp(received, payload, sizeof(payload)) == 0); + Require(std::memcmp(address.data(), expected.data(), copied) == 0); + Require(std::all_of(address.begin() + copied, address.end(), [](auto byte) { return byte == 0xa5; })); + } + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); +} + int main() { + CheckAddressLengths(2); + CheckAddressLengths(28); const int receiver = socket_nid_postfix(2, 2, 0); const int sender = socket_nid_postfix(2, 2, 17); Require(receiver >= 0 && sender >= 0 && receiver != sender); From cd067d1569cfee95097399723f8a011337a512d4 Mon Sep 17 00:00:00 2001 From: Diversao <83223857+diversao@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:00:48 -0300 Subject: [PATCH 230/318] fix(agc): release parked host imports a pinned guest range waits for A device replaced at AttachWindow keeps its host imports in HostImports::parked until it is destroyed, and each parked import keeps its guest ranges pinned. A guest munmap, mprotect or free of such a range spun in the pin waiter until APS5_PIN_WAIT_MS and then failed, because the waiter only looked at the live imports. The replaced device was already waited idle by PrepareForReplacement, so the waiter now drops the parked imports over the range. agc_parked_import_unmap imports a range on one device, again on a second one, and unmaps it with a 3 s pin wait. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/src/GuestBufferMemory.cpp | 22 ++++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 70 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6c94ffc4b..26b631e3e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -811,6 +811,8 @@ if(BUILD_TESTING) endif() add_test(NAME agc_host_import_unmap COMMAND agc_driver_recorder_tests --host-import-unmap-only) set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_parked_import_unmap COMMAND agc_driver_recorder_tests --parked-import-unmap-only) + set_tests_properties(agc_parked_import_unmap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only) @@ -822,7 +824,7 @@ if(BUILD_TESTING) add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 73e1015e5..d5a81a071 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -219,6 +219,26 @@ void retireImport(VkDevice device, HostImports& state, std::map> released; + { + std::lock_guard lock(state.mutex); + for (auto device = state.parked.begin(); device != state.parked.end();) { + auto& list = device->second; + for (auto it = list.begin(); it != list.end();) { + if ((*it)->entry.base < end && begin < (*it)->entry.base + (*it)->entry.bytes) { + released.push_back(std::move(*it)); + it = list.erase(it); + } else { + ++it; + } + } + device = list.empty() ? state.parked.erase(device) : std::next(device); + } + } + return !released.empty(); +} + const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) { const auto failed = [&](const char* step, VkResult result) -> const char* { if (entry.buffer) context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); @@ -1534,6 +1554,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept if (recorder != nullptr) recorder->FlushDeferredReleases(); const auto end = static_cast(address) + bytes; auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); { std::lock_guard importsLock(imports.mutex); @@ -1585,6 +1606,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept const auto end = static_cast(address) + bytes; auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; bool overlapsImport = false; { std::lock_guard importsLock(imports.mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 99e4f9a77..e8c9cf883 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2567,6 +2567,71 @@ void hostImportUnmapTests(const Device& device, Recorder& recorder) { std::cout << "host import unmap lifetime: ok\n"; } +void parkedImportUnmapTests(const Device& device) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0) { + std::cout << "host imports unavailable: parked import unmap not tested\n"; + return; + } + const auto alignment = static_cast(std::max(65536u, context.hostImportAlignment)); + const auto bytes = alignment * 4u; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(alignment, bytes); +#endif + Require(block != nullptr, "cannot allocate the parked import unmap block"); + const auto release = [&] { +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + }; + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + std::unique_ptr replacement; + bool imported = false; + { + std::lock_guard gpu(GpuMutex()); + imported = HostImportFor(context, address, bytes) != nullptr; + } + if (imported) { + replacement = std::make_unique(); + std::lock_guard gpu(GpuMutex()); + imported = HostImportFor(replacement->GetContext(), address, bytes) != nullptr; + } + if (!imported) { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (replacement) ClearHostImports(replacement->GetContext().device); + release(); + std::cout << "host import refused: parked import unmap not tested\n"; + return; + } + bool unmapped = false; + try { + GuestAllocations::Mutation mutation; + mutation.Unmap(block, bytes, [&](const void*, std::size_t, const void*, bool) { unmapped = true; }); + } catch (const std::exception& error) { + ClearHostImports(replacement->GetContext().device); + ClearHostImports(context.device); + GuestAllocations::Mutation mutation; + mutation.Remove(block); + release(); + throw std::runtime_error(std::string("guest unmap after a device replacement: ") + error.what()); + } + ClearHostImports(replacement->GetContext().device); + ClearImageMirrors(replacement->GetContext().device); + replacement.reset(); + release(); + Require(unmapped, "guest unmap of a range a replaced device imported did not run"); + std::cout << "parked import unmap: ok\n"; +} + void importWatchTests(const Device& device) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -5400,6 +5465,11 @@ int main(int argc, char** argv) { hostImportUnmapTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--parked-import-unmap-only") { + gpu.unlock(); + parkedImportUnmapTests(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--unit-shadow-only") { if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) { std::cout << "skipped, unit shadows or host imports are unavailable\n"; From bad49ba2c452d3a60216b4b4b5db04f047298da8 Mon Sep 17 00:00:00 2001 From: Tyler Westerberg Date: Mon, 12 Oct 2026 00:10:51 -0400 Subject: [PATCH 231/318] feat(libSceVoiceQoS): export sceVoiceQoSSetConnectionAttribute Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVoiceQoS/Export.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 1 + 2 files changed, 6 insertions(+) diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 4756143ee..e8213885a 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -65,6 +65,11 @@ int APS5_VABI sceVoiceQoSDisconnect() { return 0; } +int APS5_VABI sceVoiceQoSSetConnectionAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f5709b025..89a4979b7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -58,6 +58,7 @@ Throughout the project, every function at every stage either **does exactly what - [kern.rng_pseudo](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown - [kern.sdk_version](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - PPSA01530, PPSA23827 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), and [scePadUnknown_n3kSX62fgNo](../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. +- [sceVoiceQoSSetConnectionAttribute](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) - [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. - [sceHttp2WebSocketSetPingInterval](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. - [sceKernelGetProsperoSystemSwVersion](../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it From 2add25cea043aa4a30a4e41a230c917260957135 Mon Sep 17 00:00:00 2001 From: Tyler Westerberg Date: Sat, 10 Oct 2026 17:28:07 -0400 Subject: [PATCH 232/318] feat(agc-driver): replay a request as a prepared artifact with --prepare agc_shader_replay compiles a request through Recompile, the per-draw path. Shaders the driver prepares at registration go through PrepareShader instead and produce a different, larger artifact, so a failure or the size of that artifact could not be reproduced from the dumped request. --prepare compiles the request with PrepareShader and prints the artifact's SPIR-V size, or the first line of the failure. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tools/ShaderReplay.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index b62ee64a7..524575eef 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -41,6 +41,7 @@ bool g_disassemble = false; bool g_assembly = false; bool g_memory = false; bool g_spirv = false; +bool g_prepare = false; bool g_graph = false; bool g_maintenance8 = false; bool g_code = false; @@ -113,6 +114,16 @@ bool Replay(const char* path) { std::printf(" resource analysis failed: %s\n", FirstLine(error.what()).c_str()); return false; } + if (g_prepare) { + try { + const auto handle = ShaderRecompiler::PrepareShader(request.request); + std::printf(" prepared: %zu SPIR-V words\n", ShaderRecompiler::GetPreparedArtifact(*handle).spirv.Words().size()); + return true; + } catch (const std::exception& error) { + std::printf(" prepare failed: %s\n", FirstLine(error.what()).c_str()); + return false; + } + } try { if (g_maintenance8) request.request.target.nonConstantImageOffsets = true; const auto result = ShaderRecompiler::Recompile(request.request); @@ -152,7 +163,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--code] [--maintenance8] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--code] [--maintenance8] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); return 2; } int failures = 0; @@ -181,6 +192,10 @@ int main(int argc, char** argv) { g_code = true; continue; } + if (std::string(argv[i]) == "--prepare") { + g_prepare = true; + continue; + } if (std::string(argv[i]) == "--spv") { g_spirv = true; continue; From b42ef24e07725ba1166420073817d80a60ec9149 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:25:29 +0200 Subject: [PATCH 233/318] fix(libs): keep thread_local objects with destructors out of module teardown on Windows (#4029) * fix(agc): keep the driver's per-thread scratch out of thread_local teardown With MinGW's emulated TLS, a thread_local with a destructor in a DLL is destroyed after its storage is gone: when a std::thread ends, winpthread runs libgcc's emutls key destructor, which frees the thread's thread_local storage, and only then does the loader's DLL_THREAD_DETACH run the DLL's tls_atexit list, so the destructors run on freed memory. The queue worker touches four of them in libSceAgcDriver (the dispatch-ahead map, the pending waits, the draw-snapshot compare vectors and the image decode memo), and Driver::stop's join of the worker then corrupts the heap. Under cdb's checking heap agc_driver_draw_cache_reuse stopped at the worker's exit with heap corruption in all 127 runs, with graphics pipeline libraries, with APS5_NO_PIPELINE_LTO=1 and with APS5_NO_GPL=1. Without it the damage shows intermittently, as exit 0xc0000374 (2 of 143 runs with a cold NVIDIA shader cache) or as a fault in whatever allocates next, such as NVIDIA's compiler on the optimized-link thread (#2980). These objects now live in HostThreadLocal, fiber-local storage that owns them, as the recorder's buffers do since e424b6b7b. Co-Authored-By: Claude Opus 5.5 * fix(libs): keep thread_local objects with destructors out of module teardown The teardown order behind the driver's heap corruption holds in every module: with MinGW's emulated TLS a thread's thread_local storage is freed before the module's DLL_THREAD_DETACH runs its thread_local destructors, so they run on freed objects. PreciseSleepUs's timer (used, among others, by the video-out vblank thread) then closes a stale handle; libkernel's directory path, socket waker and UUID random device, libSceRandom's random device and libScePosixForWebKit's random device and host lookup free or close stale objects. They move to HostThreadLocal. The other thread_locals in the modules are trivially destructible or only built for Linux. Co-Authored-By: Claude Opus 5.5 * test(libs): fail when a module registers a thread_local destructor prx_thread_local_destructors lists the symbols of every built module and, where MinGW's thread_local destructor support is linked in, disassembles the module for calls to __cxa_thread_atexit outside libc's registration of guest destructors. A module that keeps a thread_local with a destructor fails it with the function that registers one: with main's libSceRandom it reports sceRandomGetRandomNumber, and with this branch all 122 modules pass, libc being the only one that links the support. Co-Authored-By: Claude Opus 5.5 * fix(agc): keep the image memo behind its pointer The image decode memo went into HostThreadLocal as the whole array. On Linux, where HostThreadLocal is a plain thread_local, that put 1024 memo entries into every thread's TLS block instead of one pointer. It keeps its unique_ptr as before, now owned by HostThreadLocal, so Linux builds the same thread_local as main. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Dean Galvin --- core/libs/CMakeLists.txt | 5 +++ .../src/Driver/Synchronization/WaitMemory.cpp | 5 ++- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 18 ++++++--- .../Graphics/src/ShaderResources.cpp | 4 +- .../prx/libScePosixForWebKit/Resolver.cpp | 4 +- core/libs/prx/libc/include/PreciseWait.hpp | 3 +- .../File/src/DirectoryDescriptor.cpp | 4 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 ++- core/libs/tests/ThreadLocalDestructors.cmake | 38 +++++++++++++++++++ 9 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 core/libs/tests/ThreadLocalDestructors.cmake diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 23a160828..64194a5cc 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -290,6 +290,11 @@ if(BUILD_TESTING) endif() add_test(NAME host_thread_local COMMAND host_thread_local_tests) set_tests_properties(host_thread_local PROPERTIES TIMEOUT 15) + if(MINGW) + add_test(NAME prx_thread_local_destructors COMMAND ${CMAKE_COMMAND} -DNM=${CMAKE_NM} -DOBJDUMP=${CMAKE_OBJDUMP} + -DMODULES=$ -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ThreadLocalDestructors.cmake) + set_tests_properties(prx_thread_local_destructors PROPERTIES TIMEOUT 300) + endif() add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp) target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp index 1475997dd..9d15cf04b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -54,8 +55,8 @@ bool Driver::traceLateLabels() { } std::vector& Driver::pendingWaits() { - static thread_local std::vector waits; - return waits; + struct WaitsTag {}; + return HostThreadLocal, WaitsTag>(); } void Driver::notePendingWait(std::span packet, std::uint32_t queue, std::uint64_t received) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 4c660d44f..cfcb1c010 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2506,11 +2506,19 @@ bool Recorder::refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, s const auto aligned = address / block * block; const auto end = address + bytes; const auto count = static_cast((end - 1) / block - address / block + 1); - thread_local std::vector generations; - thread_local std::vector changed; - thread_local std::vector> differing; - thread_local std::vector spans; - thread_local std::vector equal; + struct Scratch { + std::vector generations; + std::vector changed; + std::vector> differing; + std::vector spans; + std::vector equal; + }; + auto& scratch = HostThreadLocal(); + auto& generations = scratch.generations; + auto& changed = scratch.changed; + auto& differing = scratch.differing; + auto& spans = scratch.spans; + auto& equal = scratch.equal; generations.assign(count, entry.generation); changed.assign(count, GuestMemory::BlockWritten); differing.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 0383b7571..6c1d10286 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "ThreadOwned.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" @@ -278,9 +279,10 @@ struct ImageMemoEntry { }; constexpr std::size_t ImageMemoSlots = 1024; -thread_local std::unique_ptr> imageMemo; +struct ImageMemoTag {}; ImageMemoEntry& imageMemoSlot(std::span words) { + auto& imageMemo = HostThreadLocal>, ImageMemoTag>(); if (imageMemo == nullptr) imageMemo = std::make_unique>(); const auto hash = hashWords(14695981039346656037ull, words.data(), words.size()); return (*imageMemo)[(hash ^ (hash >> 29u)) % ImageMemoSlots]; diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 7c5254fee..277290301 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -8,6 +8,7 @@ #include #endif #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -129,7 +130,8 @@ GuestResolver::HostEntry* APS5_VABI gethostbyname_nid_postfix(const char* name) return nullptr; } #endif - static thread_local HostLookup lookup; + struct LookupTag {}; + auto& lookup = HostThreadLocal(); lookup.name = native->h_name; lookup.aliasNames.clear(); lookup.aliases.clear(); diff --git a/core/libs/prx/libc/include/PreciseWait.hpp b/core/libs/prx/libc/include/PreciseWait.hpp index e9fc32421..73249384b 100644 --- a/core/libs/prx/libc/include/PreciseWait.hpp +++ b/core/libs/prx/libc/include/PreciseWait.hpp @@ -6,6 +6,7 @@ #ifdef _WIN32 #include +#include "prx/libc/include/HostThreadLocal.hpp" #endif inline constexpr unsigned long long kSubTickWaitUs = 2000; @@ -24,7 +25,7 @@ inline void PreciseSleepUs(unsigned long long micros) { } } }; - thread_local Timer timer; + auto& timer = HostThreadLocal(); if (timer.handle != nullptr) { LARGE_INTEGER due{}; due.QuadPart = -static_cast(micros * 10ULL); diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 9bbe6569b..d9271c309 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include @@ -177,7 +178,8 @@ extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* erro *error = ENOTDIR; return nullptr; } - static thread_local std::string path; + struct PathTag {}; + auto& path = HostThreadLocal(); const auto utf8Path = directory->u8string(); path.assign(reinterpret_cast(utf8Path.data()), utf8Path.size()); *error = 0; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 0ac8f3163..436c88e28 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -14,6 +14,7 @@ #endif #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" @@ -110,8 +111,8 @@ struct Waker { } }; Waker& ThreadWaker() { - thread_local Waker waker; - return waker; + struct WakerTag {}; + return HostThreadLocal(); } std::mutex socketsMutex; std::map> sockets; diff --git a/core/libs/tests/ThreadLocalDestructors.cmake b/core/libs/tests/ThreadLocalDestructors.cmake new file mode 100644 index 000000000..b87abde07 --- /dev/null +++ b/core/libs/tests/ThreadLocalDestructors.cmake @@ -0,0 +1,38 @@ +file(GLOB modules "${MODULES}/*.prx") +if(NOT modules) + message(FATAL_ERROR "no modules in ${MODULES}") +endif() +set(explicit "__cxa_thread_atexit_impl_nid_postfix|LibcInternalExtCxaThreadAtexit_nid_postfix") +set(failures "") +foreach(module IN LISTS modules) + get_filename_component(name "${module}" NAME) + execute_process(COMMAND "${NM}" --defined-only "${module}" OUTPUT_VARIABLE symbols RESULT_VARIABLE result ERROR_QUIET) + if(NOT result EQUAL 0 OR symbols STREQUAL "") + list(APPEND failures "${name}: no symbol table to inspect") + continue() + endif() + if(NOT symbols MATCHES " T __mingw_cxa_thread_atexit\n") + continue() + endif() + execute_process(COMMAND "${OBJDUMP}" -d --no-show-raw-insn "${module}" OUTPUT_VARIABLE code RESULT_VARIABLE result ERROR_QUIET) + if(NOT result EQUAL 0) + list(APPEND failures "${name}: cannot disassemble") + continue() + endif() + string(REGEX MATCHALL "\n[0-9a-f]+ <[^>\n]+>:|(call|jmp)[^\n]*<__cxa_thread_atexit>" lines "${code}") + set(function "") + foreach(line IN LISTS lines) + if(line MATCHES "^\n[0-9a-f]+ <([^>\n]+)>:$") + set(function "${CMAKE_MATCH_1}") + elseif(NOT function MATCHES "^(${explicit})$") + list(APPEND failures "${name}: ${function} registers a thread_local destructor") + endif() + endforeach() +endforeach() +if(failures) + list(REMOVE_DUPLICATES failures) + list(JOIN failures "\n " text) + message(FATAL_ERROR "On Windows a thread_local destructor in a module runs after emulated TLS freed the object; keep such objects in HostThreadLocal:\n ${text}") +endif() +list(LENGTH modules count) +message(STATUS "${count} modules register no thread_local destructors") From 661f8e1e7c31f7c874baecdcf461a9b80e2e3999 Mon Sep 17 00:00:00 2001 From: Utku Halis Date: Mon, 12 Oct 2026 08:26:47 +0300 Subject: [PATCH 234/318] feat(libSceAgc): implement sceAgcAcbWaitUntilSafeForRendering --- core/libs/prx/libSceAgc/Acb/src/Control.cpp | 12 +++--- core/libs/prx/libSceAgc/tests/Commands.cpp | 12 ++++++ .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Flip.cpp | 42 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index a4692c619..cfacb3c60 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgc/DcbState/include/Marker.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include #include #include "SceTypes.hpp" @@ -51,11 +52,12 @@ std::uint32_t APS5_VABI sceAgcAcbRewindGetSize() { } std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t videoOutHandle, std::uint32_t displayBufferIndex) { - (void)buf; - (void)videoOutHandle; - (void)displayBufferIndex; - NotImplemented_nid_no_patch(__func__); - return nullptr; + auto* packet = Agc::Command::Allocate(buf, AgcDriver::RenderingWaitPacketWords, __func__); + packet[0] = AgcDriver::RenderingWaitPacketHeader; + packet[1] = videoOutHandle; + packet[2] = displayBufferIndex; + packet[3] = 0; + return packet; } std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t videoOutHandle, std::int32_t displayBufferIndex, std::uint32_t flipMode, std::int64_t flipArg) { diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index d3d9c7f83..8bf2d5fe0 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -13,6 +13,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state); extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t handle, std::uint32_t index); extern "C" int APS5_VABI sceAgcSuspendPoint(); extern "C" int APS5_VABI sceAgcInit(std::uint32_t version); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults(); @@ -317,6 +318,17 @@ void testFlip() { exhausted.buffer.cursor_down = exhausted.words.data() + 5; expectFailure([&] { sceAgcDcbSetFlip(&exhausted.buffer, 1, 0, 1, 0); }); check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed flip allocation advanced cursor"); + Storage acb; + acb.words.fill(0xdeadbeefu); + auto* wait = sceAgcAcbWaitUntilSafeForRendering(&acb.buffer, 0xfedcba98u, 2); + const std::array expectedWait{0xc0021018u, 0xfedcba98u, 2, 0}; + check(wait == acb.words.data() && std::equal(expectedWait.begin(), expectedWait.end(), wait), "compute rendering wait packet mismatch"); + check(acb.buffer.cursor_up == wait + 4 && wait[4] == 0xdeadbeefu, "compute rendering wait overran allocation"); + expectFailure([] { sceAgcAcbWaitUntilSafeForRendering(nullptr, 1, 0); }); + Storage full; + full.buffer.cursor_down = full.words.data() + 3; + expectFailure([&] { sceAgcAcbWaitUntilSafeForRendering(&full.buffer, 1, 0); }); + check(full.buffer.cursor_up == full.words.data(), "failed compute rendering wait advanced cursor"); check(sceAgcSuspendPoint() == 0, "empty suspend failed"); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 9b7b06e0a..2fdd6b930 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -249,7 +249,7 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[1] & 0xffff0000u) != 0x68750000u, "typed user-data and legacy flip markers are not implemented"); break; case 0x09: size(2); break; - case 0x06: graphics(); size(4); require(packet[3] == 0, "unsupported rendering wait mode"); break; + case 0x06: size(4); require(packet[3] == 0, "unsupported rendering wait mode"); break; case 0x0b: { const auto data = std::as_bytes(packet.subspan(1)); require(std::find(data.begin(), data.end(), std::byte{}) != data.end(), "unterminated marker text"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index 4ce4faa60..8b3823728 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -265,6 +265,46 @@ void testFlipHold() { AgcDriverUnregisterVideoOutput_nid_postfix(7, output); } +void testComputeRenderingWait() { + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array wait{0xc0021018, 8, 0, 0}; + Packet packet{wait.data(), 4, 0, {}}; + check(expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); }).find("unregistered video output") != std::string::npos, "a compute rendering wait on an unregistered output was accepted"); + wait = {0xc0021018, 7, 0, 1}; + check(expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); }).find("rendering wait mode") != std::string::npos, "a compute rendering wait with a mode was accepted"); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = true; + } + struct GateRelease { + std::shared_ptr state; + ~GateRelease() { + { + std::lock_guard lock(state->mutex); + state->gate = false; + } + state->changed.notify_all(); + } + } gateRelease{output->state}; + wait = {0xc0021018, 7, 0, 0}; + check(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute rendering wait submission failed"); + { + std::unique_lock lock(output->state->mutex); + check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->waiting; }), "compute worker did not reach the rendering wait"); + } + auto idle = std::async(std::launch::async, [] { AgcDriverWaitIdle_nid_postfix(); }); + check(idle.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout, "the driver went idle past a closed compute rendering wait"); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = false; + } + output->state->changed.notify_all(); + check(idle.wait_for(std::chrono::seconds(5)) == std::future_status::ready, "a compute rendering wait stayed blocked after release"); + idle.get(); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); +} + void testFailure() { auto output = std::make_shared(); output->state->fail = true; @@ -301,7 +341,7 @@ void testReset(bool compute) { int main(int argc, char** argv) { try { if (argc == 2) testReset(std::string(argv[1]) == "compute"); - else { testFlipAndBoundary(); testFlipHold(); testFailure(); } + else { testFlipAndBoundary(); testFlipHold(); testComputeRenderingWait(); testFailure(); } const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); check(shutdown.find(argc == 2 ? (std::string(argv[1]) == "compute" ? "not readable" : "required shader register") : "intentional flip failure") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC flip and suspend tests passed"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c1247e46b..01ca191a3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -369,6 +369,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAppContentGetAddcontDownloadProgress](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - imported by PPSA08755, which fails to load without it; added without an implementation because its behavior is unverified. [SharpProspero](https://github.com/SvenGDK/SharpProspero) lists `(uint32_t serviceLabel, entitlementLabel, SceAppContentAddcontDownloadProgress*)`, but no PS5 reference gives the progress layout or the error codes - [_sceFiberGetThreadFramePointerAddress](../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured +- [sceAgcAcbWaitUntilSafeForRendering](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known ### Functional From 5b7cb8d12688cec03ac2a6ac6963a898394aa3b2 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 17:06:49 +1300 Subject: [PATCH 235/318] perf(libkernel): find direct mappings that share physical memory through an index --- .../libkernel/DirectMemory/DirectMemory.cpp | 92 ++++++++++++++++--- core/libs/tests/GuestMemory.cpp | 87 ++++++++++++++++++ 2 files changed, 167 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index d5c2bbd1c..a04f158f7 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -348,15 +348,77 @@ struct DirectMapping { std::map g_directMappings; +#if defined(__linux__) +struct PhysicalSpan { + std::uint64_t end; + std::vector bases; +}; + +std::map g_physicalSpans; + +void SplitPhysicalSpan(std::uint64_t at) { + auto span = g_physicalSpans.upper_bound(at); + if (span == g_physicalSpans.begin()) return; + --span; + if (span->first == at || span->second.end <= at) return; + g_physicalSpans.emplace_hint(std::next(span), at, PhysicalSpan{span->second.end, span->second.bases}); + span->second.end = at; +} +#endif + +std::map::iterator InsertMapping(std::uintptr_t base, DirectMapping mapping) { + const auto [inserted, added] = g_directMappings.emplace(base, std::move(mapping)); + if (!added) throw std::runtime_error("direct mapping inserted over a live one"); +#if defined(__linux__) + const auto first = inserted->second.phys; + const auto last = first + (inserted->second.end - base); + SplitPhysicalSpan(first); + SplitPhysicalSpan(last); + auto span = g_physicalSpans.lower_bound(first); + for (auto cursor = first; cursor < last; ++span) { + if (span == g_physicalSpans.end() || span->first > cursor) span = g_physicalSpans.emplace_hint(span, cursor, PhysicalSpan{span == g_physicalSpans.end() ? last : std::min(last, span->first), {}}); + auto& bases = span->second.bases; + bases.insert(std::upper_bound(bases.begin(), bases.end(), base), base); + cursor = span->second.end; + } +#endif + return inserted; +} + +std::map::iterator RemoveMapping(std::map::iterator mapping) { +#if defined(__linux__) + const auto base = mapping->first; + const auto first = mapping->second.phys; + const auto last = first + (mapping->second.end - base); + for (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.end() && span->first < last;) { + auto& bases = span->second.bases; + bases.erase(std::lower_bound(bases.begin(), bases.end(), base)); + span = bases.empty() ? g_physicalSpans.erase(span) : std::next(span); + } + for (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.end() && span->first <= last;) { + if (span != g_physicalSpans.begin()) { + const auto before = std::prev(span); + if (before->second.end == span->first && before->second.bases == span->second.bases) { + before->second.end = span->second.end; + span = g_physicalSpans.erase(span); + continue; + } + } + ++span; + } +#endif + return g_directMappings.erase(mapping); +} + void EraseMappings(std::uintptr_t start, std::uintptr_t end) { auto it = g_directMappings.lower_bound(start); if (it != g_directMappings.begin() && std::prev(it)->second.end > start) --it; while (it != g_directMappings.end() && it->first < end) { const auto base = it->first; const auto mapping = it->second; - it = g_directMappings.erase(it); - if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys, mapping.memoryType, mapping.backing}); - if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; + it = RemoveMapping(it); + if (base < start) InsertMapping(base, DirectMapping{start, mapping.phys, mapping.memoryType, mapping.backing}); + if (mapping.end > end) it = InsertMapping(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}); } } @@ -369,14 +431,14 @@ void ErasePhysMappings(std::uint64_t first, std::uint64_t last) { ++it; continue; } - it = g_directMappings.erase(it); + it = RemoveMapping(it); if (mapping.phys < first) { const auto keep = first - mapping.phys; - g_directMappings.emplace(base, DirectMapping{base + keep, mapping.phys, mapping.memoryType, mapping.backing}); + InsertMapping(base, DirectMapping{base + keep, mapping.phys, mapping.memoryType, mapping.backing}); } if (mappingLast > last) { const auto skip = last - mapping.phys; - it = g_directMappings.emplace(base + skip, DirectMapping{mapping.end, last, mapping.memoryType, mapping.backing}).first; + it = InsertMapping(base + skip, DirectMapping{mapping.end, last, mapping.memoryType, mapping.backing}); } } } @@ -396,10 +458,16 @@ void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { void WatchMapping(std::uintptr_t address, std::size_t len) { GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(address), len); const auto end = address + len; + std::vector sharing; for (auto added = g_directMappings.lower_bound(address); added != g_directMappings.end() && added->first < end; ++added) { const auto addedPhys = added->second.phys; const auto addedPhysEnd = addedPhys + (added->second.end - added->first); - for (const auto& [base, other] : g_directMappings) { + sharing.clear(); + for (auto span = g_physicalSpans.lower_bound(addedPhys); span != g_physicalSpans.end() && span->first < addedPhysEnd; ++span) sharing.insert(sharing.end(), span->second.bases.begin(), span->second.bases.end()); + std::sort(sharing.begin(), sharing.end()); + sharing.erase(std::unique(sharing.begin(), sharing.end()), sharing.end()); + for (const auto base : sharing) { + const auto& other = g_directMappings.at(base); if ((base >= address && base < end) || other.backing != added->second.backing) continue; const auto first = std::max(addedPhys, other.phys); const auto last = std::min(addedPhysEnd, other.phys + (other.end - base)); @@ -481,7 +549,7 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int bytes += PS5_PAGE_SIZE; } page.backing->Map(address + offset, bytes, page.offset, nativeProt); - g_directMappings.emplace(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType(), page.backing}); + InsertMapping(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType(), page.backing}); offset += bytes; } #if defined(__linux__) @@ -945,13 +1013,13 @@ int DoMtypeprotect(const void* addr, size_t len, int type, int prot) { while (it != g_directMappings.end() && it->first < end) { const auto base = it->first; const auto mapping = it->second; - it = g_directMappings.erase(it); - if (base < first) g_directMappings.emplace(base, DirectMapping{first, mapping.phys, mapping.memoryType, mapping.backing}); + it = RemoveMapping(it); + if (base < first) InsertMapping(base, DirectMapping{first, mapping.phys, mapping.memoryType, mapping.backing}); const auto low = std::max(base, first); const auto high = std::min(mapping.end, end); - g_directMappings.emplace(low, DirectMapping{high, mapping.phys + low - base, type, mapping.backing}); + InsertMapping(low, DirectMapping{high, mapping.phys + low - base, type, mapping.backing}); physical.emplace_back(mapping.phys + low - base, high - low); - if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; + if (mapping.end > end) it = InsertMapping(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}); } } for (const auto& [phys, bytes] : physical) DirectMemoryRetype(static_cast(phys), bytes, type); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index d1fafa044..4b702fc6a 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #endif #ifdef _WIN32 @@ -1604,6 +1605,90 @@ static void CheckDirectMemoryWriteWatch() { Require(sceKernelMunmap(flexible, page * 3) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); } + +static void CheckDirectMemoryWriteWatchFollowsRemovedMappings() { + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::puts("write watch unavailable: removed direct mappings not tested"); + return; + } + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* whole = nullptr; + Require(sceKernelMapDirectMemory(&whole, page * 4, 3, 0, phys, 0) == 0); + const auto address = reinterpret_cast(whole); + const auto watched = [](const void* pointer, std::size_t offset) { return GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(pointer) + offset, page); }; + void* pair = nullptr; + Require(sceKernelMapDirectMemory(&pair, page * 2, 3, 0, phys + page, 0) == 0); + Require(watched(whole, 0) && !watched(whole, page) && !watched(whole, page * 2) && watched(whole, page * 3)); + Require(!watched(pair, 0) && !watched(pair, page)); + Require(sceKernelMunmap(pair, page) == 0); + void* single = nullptr; + Require(sceKernelMapDirectMemory(&single, page, 3, 0, phys + page, 0) == 0); + Require(!watched(single, 0) && watched(whole, 0) && watched(whole, page * 3)); + Require(sceKernelMunmap(single, page) == 0); + Require(sceKernelMunmap(static_cast(pair) + page, page) == 0); + void* replaced = whole; + Require(sceKernelMapDirectMemory(&replaced, page, 3, 0x10, phys + page * 3, 0) == 0 && replaced == whole); + Require(!watched(whole, 0) && !watched(whole, page * 3)); + std::uintptr_t start = 0; + std::uintptr_t end = 0; + std::uint64_t offset = 0; + int type = 0; + Require(QueryDirectMapping(address, &start, &end, &offset, &type) && start == address && end == address + page && offset == static_cast(phys) + page * 3); + void* alone = nullptr; + Require(sceKernelMapDirectMemory(&alone, page, 3, 0, phys, 0) == 0); + Require(watched(alone, 0)); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, page, 3, 0, phys, 0) == 0); + Require(!watched(alone, 0) && !watched(again, 0)); + Require(sceKernelMunmap(again, page) == 0); + Require(sceKernelMunmap(alone, page) == 0); + Require(sceKernelMunmap(whole, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +} + +static double MapAcrossBackingsSeconds(std::int64_t phys, std::size_t backings) { + constexpr std::size_t page = 0x4000; + const auto threadSeconds = [] { + timespec now{}; + Require(clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now) == 0); + return static_cast(now.tv_sec) + static_cast(now.tv_nsec) * 1e-9; + }; + auto best = std::numeric_limits::max(); + for (int attempt = 0; attempt < 3; ++attempt) { + void* mapped = nullptr; + const auto before = threadSeconds(); + Require(sceKernelMapDirectMemory(&mapped, page * backings, 3, 0, phys, 0) == 0); + best = std::min(best, threadSeconds() - before); + const auto last = reinterpret_cast(mapped) + page * (backings - 1); + std::uintptr_t start = 0; + std::uintptr_t end = 0; + std::uint64_t offset = 0; + int type = 0; + Require(QueryDirectMapping(last, &start, &end, &offset, &type) && start == last && end == last + page); + Require(sceKernelMunmap(mapped, page * backings) == 0); + } + return best; +} + +static void CheckMappingCostFollowsTheMappingCount() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t few = 1024; + constexpr std::size_t many = few * 16; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * many, 0, 0, &phys) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * many) == 0); + for (std::size_t index = 0; index < many; ++index) { + const auto wanted = phys + static_cast(page * index); + std::int64_t piece = 0; + Require(sceKernelAllocateDirectMemory(wanted, wanted + static_cast(page), page, 0, 0, &piece) == 0 && piece == wanted); + } + const auto small = MapAcrossBackingsSeconds(phys, few); + const auto large = MapAcrossBackingsSeconds(phys, many); + Require(large <= small * 48); + Require(sceKernelReleaseDirectMemory(phys, page * many) == 0); +} #endif static void CheckVirtualQuerySplitFlexibleRanges() { @@ -1789,6 +1874,8 @@ int main() { CheckWriteWatchArmedCollect(); CheckWriteWatchFreshCollect(); CheckDirectMemoryWriteWatch(); + CheckDirectMemoryWriteWatchFollowsRemovedMappings(); + CheckMappingCostFollowsTheMappingCount(); CheckDirectMemoryBackingNeedsNoFilesystem(); CheckDirectMemorySharedBacking(); #endif From 216984afc60665bc73fba8dc46ceb4603390def0 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 12 Oct 2026 07:37:44 +0200 Subject: [PATCH 236/318] feat(libkernel): implement sceKernelGetCpuTemperature --- core/libs/prx/libkernel/Unimplemented.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 1 + 2 files changed, 6 insertions(+) diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 9fa34427b..eb776741a 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -10,6 +10,11 @@ int APS5_VABI sceCoredumpWriteUserData() { return 0; } +int APS5_VABI sceKernelGetCpuTemperature(int *out_celcius) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI __tls_get_addr_nid_postfix(void) { NotImplemented_nid_no_patch("vNe1w4diLCs"); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f5709b025..d713be51e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -336,6 +336,7 @@ Throughout the project, every function at every stage either **does exactly what - [pnz21I6VjDs](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [qk043QPsrZU](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [uDGXwUKqi8s](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [sceKernelGetCpuTemperature](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PPSA99902, which fails to load without it; added without an implementation because its behavior is unverified. - [LinuxProtFromSce](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown From 749541f65326916cfda313243d46c8e8b3f4d764 Mon Sep 17 00:00:00 2001 From: winstrex <79346005+winstrex@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:39:44 -0300 Subject: [PATCH 237/318] feat(libkernel): implement sceKernelEnableDmemAliasing Direct memory can already be mapped more than once, so there is no aliasing to enable and the call returns 0. ProsperoEden (PPSA99008) imports it and calls it before mapping the same direct memory again. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/DirectMemory/Export.cpp | 4 ++++ core/libs/tests/GuestMemory.cpp | 2 ++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 7 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 7deed55ab..cf0fe1203 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -171,6 +171,10 @@ size_t APS5_VABI sceKernelGetDirectMemorySize(void) { return DIRECT_MEMORY_SIZE; } +int APS5_VABI sceKernelEnableDmemAliasing(void) { + return 0; +} + int APS5_VABI sceKernelMapDirectMemory(void** addr, size_t len, int prot, int flags, int64_t direct_memory_start, size_t alignment) { (void)alignment; return DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index d1fafa044..52b3867e1 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -59,6 +59,7 @@ int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const cha int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelEnableDmemAliasing(); int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); int APS5_VABI sceKernelAvailableDirectMemorySize(std::int64_t, std::int64_t, std::size_t, std::int64_t*, std::size_t*); int APS5_VABI sceKernelCheckedReleaseDirectMemory(std::int64_t, std::size_t); @@ -251,6 +252,7 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelMapDirectMemory(&first, page * 2, 3, 0, phys, 0) == 0); static_cast(first)[0] = 11; static_cast(first)[page + 5] = 22; + Require(sceKernelEnableDmemAliasing() == 0); void* alias = nullptr; Require(sceKernelMapDirectMemory(&alias, page, 3, 0, phys + page, 0) == 0); Require(alias != first && static_cast(alias)[5] == 22); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f5709b025..9ce251a0c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -47,6 +47,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - [sceAudioInInit](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 without library-wide setup, as `sceAudioOutInit` does; each `sceAudioInOpen` opens its own SDL capture device - [sceRazorCpuPushMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 +- [sceKernelEnableDmemAliasing](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified ### Unknown function info From abc1cfa57d49210837b0e675d71a582d67f47da2 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 18:54:34 +1300 Subject: [PATCH 238/318] fix(agc-driver): queue frame dumps at the dump scale A frame presented from guest memory with APS5_DUMP_FRAMES is read back at the size of the display buffer, and that whole copy went into the dump writer's queue with the scale beside it (31.6 MiB for a 3840x2160 buffer). The queue holds 64 MiB, so the third frame the writer had not finished ended the run with "frame dump queue exceeded 64 MiB". A frame blitted from the resident image is already reduced on the GPU and queued at the reduced size. The readback is now reduced by the dump scale when the slot retires and queued at that size, the same pixels the writer kept before. With APS5_DUMP_SCALE=1 nothing is reduced and the limit is reached as before. video_out_frame_dump_queue presents four 3840x2160 frames from guest memory while the writer is held on the first file and compares every dump with the decoded display buffer. --- .../Execution/src/VulkanDevice.cpp | 16 +- core/libs/prx/libSceVideoOut/CMakeLists.txt | 10 ++ .../libSceVideoOut/tests/FrameDumpQueue.cpp | 160 ++++++++++++++++++ 3 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index fa9d8b151..9c65aa876 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -591,13 +591,19 @@ struct VulkanDevice::State { void WriteFrameDump(const PresentSlot& slot) { require(slot.dumpBuffer != nullptr, "frame dump was not recorded"); - std::vector full(slot.dumpBuffer->Bytes().begin(), slot.dumpBuffer->Bytes().end()); + const auto full = slot.dumpBuffer->Bytes(); const auto index = slot.dumpIndex; - const auto width = slot.dumpWidth; - const auto height = slot.dumpHeight; + const auto scale = slot.dumpScale; + require(scale != 0 && full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid frame dump pixels"); + const auto width = (slot.dumpWidth + scale - 1) / scale; + const auto height = (slot.dumpHeight + scale - 1) / scale; + std::vector pixels(static_cast(width) * height * 4); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) std::memcpy(pixels.data() + (static_cast(y) * width + x) * 4, full.data() + (static_cast(y) * scale * slot.dumpWidth + static_cast(x) * scale) * 4, 4); + } static const auto start = std::chrono::steady_clock::now(); - std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, width, height, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); - dumpWriter.Enqueue(index, width, height, slot.dumpScale, std::move(full)); + std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + dumpWriter.Enqueue(index, width, height, 1, std::move(pixels)); } // Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 7485b2123..8a345b532 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -51,6 +51,16 @@ if(BUILD_TESTING) endforeach() endif() +if(BUILD_TESTING AND NOT WIN32) + add_test_executable(video_out_frame_dump_queue_tests tests/FrameDumpQueue.cpp) + target_include_directories(video_out_frame_dump_queue_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + target_link_libraries(video_out_frame_dump_queue_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + set_target_properties(video_out_frame_dump_queue_tests PROPERTIES CXX_EXTENSIONS OFF) + add_test(NAME video_out_frame_dump_queue COMMAND video_out_frame_dump_queue_tests) + set_tests_properties(video_out_frame_dump_queue PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 + ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=4") +endif() + if(BUILD_TESTING) add_test_executable(pad_sdl_tests tests/PadSdl.cpp) target_link_libraries(pad_sdl_tests PRIVATE SDL2-static) diff --git a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp new file mode 100644 index 000000000..2c5b74ad1 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp @@ -0,0 +1,160 @@ +#include "prx/libSceVideoOut/include/VideoOutDriver.hpp" +#include "prx/libSceVideoOut/include/Buffer.hpp" +#include "prx/libSceVideoOut/include/Output.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef main + +namespace { + +constexpr std::uint32_t Width = 3840; +constexpr std::uint32_t Height = 2160; +constexpr std::uint32_t Scale = 4; +constexpr std::uint32_t DumpWidth = Width / Scale; +constexpr std::uint32_t DumpHeight = Height / Scale; +constexpr std::size_t DumpBytes = 54 + static_cast(DumpWidth) * DumpHeight * 4; +constexpr int Frames = 4; + +void check(bool condition, const char* reason) { + if (!condition) throw std::runtime_error(reason); +} + +std::string frameName(int index) { + char name[32]; + std::snprintf(name, sizeof(name), "frame_%03d.bmp", index); + return name; +} + +class HeldDump final { + int descriptor; + +public: + explicit HeldDump(int index) { + const auto name = frameName(index); + check(mkfifo(name.c_str(), 0600) == 0, "a frame dump pipe could not be created"); + descriptor = open(name.c_str(), O_RDWR); + check(descriptor >= 0, "a frame dump pipe could not be opened"); + } + + ~HeldDump() { close(descriptor); } + + HeldDump(const HeldDump&) = delete; + HeldDump& operator=(const HeldDump&) = delete; + + std::vector Release() const { + std::vector dump(DumpBytes); + for (std::size_t done = 0; done < dump.size();) { + pollfd waiting{descriptor, POLLIN, 0}; + check(poll(&waiting, 1, 15000) == 1, "a queued frame dump was not written"); + const auto count = read(descriptor, dump.data() + done, dump.size() - done); + check(count > 0, "a frame dump pipe could not be read"); + done += static_cast(count); + } + return dump; + } +}; + +std::vector readDump(int index) { + std::ifstream file(frameName(index), std::ios::binary); + check(file.is_open(), "a frame dump is missing"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void checkDump(std::span dump, std::span decoded) { + check(dump.size() == DumpBytes, "a frame dump has the wrong size"); + std::uint32_t extent[2]; + std::memcpy(extent, dump.data() + 18, sizeof(extent)); + check(extent[0] == DumpWidth && extent[1] == static_cast(-static_cast(DumpHeight)), "a frame dump has the wrong extent"); + for (std::uint32_t y = 0; y < DumpHeight; ++y) { + for (std::uint32_t x = 0; x < DumpWidth; ++x) { + const auto dumped = dump.data() + 54 + (static_cast(y) * DumpWidth + x) * 4; + const auto source = decoded.data() + (static_cast(y) * Scale * Width + static_cast(x) * Scale) * 4; + check(std::memcmp(dumped, source, 4) == 0, "a frame dump pixel differs from the display buffer"); + } + } +} + +void testDumpQueue() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::vector allocation(static_cast((Width + 127) / 128) * ((Height + 127) / 128) * 65536 + 65535); + const auto address = reinterpret_cast(allocation.data()); + const std::span storage{allocation.data() + ((65536u - (address & 65535u)) & 65535u), allocation.size() - 65535u}; + const AgcDriver::DisplayBuffer display{reinterpret_cast(storage.data()), 0x8000000000000000ull, Width, Height}; + check(AgcDriver::DisplayBufferSize(display) == storage.size(), "incorrect padded display footprint"); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = AgcDriver::DisplayBufferOffset(display, x, y); + storage[offset] = static_cast(x & 255u); + storage[offset + 1] = static_cast(y & 255u); + storage[offset + 2] = static_cast((x >> 8u) | ((y >> 8u) << 4u)); + storage[offset + 3] = std::byte{255}; + } + } + const auto decoded = AgcDriver::ReadDisplayBuffer(display); + VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, display.pixelFormat, 0, Width, Height, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "buffer registration failed"); + const HeldDump first(0); + const HeldDump last(Frames - 1); + std::exception_ptr failure; + try { + for (int frame = 0; frame < Frames; ++frame) { + check(sceVideoOutSubmitFlip(handle, 0, 1, frame) == 0, "the flip was not accepted"); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == static_cast(frame) + 1; }), "the flip did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + } + } catch (...) { + failure = std::current_exception(); + } + const auto firstDump = first.Release(); + if (failure) std::rethrow_exception(failure); + checkDump(firstDump, decoded); + const auto lastDump = last.Release(); + checkDump(lastDump, decoded); + for (int frame = 1; frame < Frames - 1; ++frame) checkDump(readDump(frame), decoded); + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); +} + +} + +int main() { + VideoOutTestEnvironment environment; + try { + testDumpQueue(); + std::puts("VideoOut frame dump queue test passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + if (std::string(error.what()).find("Vulkan support") != std::string::npos && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) { + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + return 77; + } + return 1; + } +} From deaa2e3ef69449e9e43194082709bc396d8fa200 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:59:29 +0200 Subject: [PATCH 239/318] test(agc): draw into a 1_5_5_5 target and read the 16 bits back Draws 32_ABGR exports into a tiled and a linear format 17 target and compares every texel with the layout X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, including the X/Z-swapped pairs, rounding edges and texels outside the scissor. Reverting the X/Z exchange fails it. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../tests/execution/Color1555Target.cpp | 299 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/Color1555Target.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..281171cf4 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -954,6 +954,8 @@ target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $) +target_include_directories(agc_driver_color1555_target_tests PRIVATE $) + foreach(stage IN ITEMS vert frag) set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage}) set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Color1555Target.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Color1555Target.cpp new file mode 100644 index 000000000..e4cf4c69b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Color1555Target.cpp @@ -0,0 +1,299 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Color1555Info = 0x8044u; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 16> Programs{}; +std::size_t programCount = 0; + +using Color = std::array; + +std::uint16_t Texel(const Color& color) { + const auto unorm = [](float value, std::uint32_t maximum) { return static_cast(std::lround(std::fmin(std::fmax(static_cast(value), 0.0), 1.0) * maximum)); }; + return static_cast(unorm(color[0], 31) | (unorm(color[1], 31) << 5u) | (unorm(color[2], 31) << 10u) | (unorm(color[3], 1) << 15u)); +} + +std::span Program(const Color& color) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + code[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + code[at++] = std::bit_cast(color[channel]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Color1555Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && color.elementBytes == 2 && color.bytes == block.bytes, "the 1_5_5_5 unorm target did not decode as an A1R5G5B5 surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId, "the draw did not use the registered pixel shader"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::vector Fill(Block& block) { + std::vector halves(block.bytes / 2u); + for (std::size_t index = 0; index < halves.size(); ++index) halves[index] = static_cast((index * 0x9e3779b9u) >> 16u); + std::memcpy(block.data, halves.data(), block.bytes); + return halves; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint16_t texel, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool inside = x < ScissorWidth; + const auto expected = inside ? texel : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 2); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3); + auto before = Fill(block); + struct Case { + Color color; + std::uint16_t texel; + }; + const std::array cases{{ + {{1.0f, 0.0f, 0.0f, 1.0f}, 0x801f}, + {{0.0f, 1.0f, 0.0f, 0.0f}, 0x03e0}, + {{0.0f, 0.0f, 1.0f, 0.0f}, 0x7c00}, + {{0.0f, 0.0f, 0.0f, 1.0f}, 0x8000}, + {{1.0f, 0.0f, 1.0f, 0.0f}, 0x7c1f}, + {{1.0f, 1.0f, 1.0f, 1.0f}, 0xffff}, + {{0.0f, 0.0f, 0.0f, 0.0f}, 0x0000}, + {{3.0f / 31.0f, 10.0f / 31.0f, 21.0f / 31.0f, 1.0f}, (3u) | (10u << 5u) | (21u << 10u) | 0x8000u}, + {{21.0f / 31.0f, 3.0f / 31.0f, 10.0f / 31.0f, 0.0f}, (21u) | (3u << 5u) | (10u << 10u)}, + {{1.0f / 31.0f, 30.0f / 31.0f, 2.0f / 31.0f, 0.75f}, (1u) | (30u << 5u) | (2u << 10u) | 0x8000u}, + {{2.0f, -1.0f, 10.0f / 31.0f, 0.25f}, (31u) | (0u << 5u) | (10u << 10u)}, + }}; + for (const auto& test : cases) { + Require(Texel(test.color) == test.texel, "the test's expected texel disagrees with the 1_5_5_5 field layout"); + Draw(device, color, Float32Export, Program(test.color)); + char what[160]; + std::snprintf(what, sizeof(what), "a 32_ABGR export of (%.9g, %.9g, %.9g, %.9g) into a %s 1_5_5_5 unorm target", test.color[0], test.color[1], test.color[2], test.color[3], kind); + ExpectSurface(device, block, layout, before, test.texel, what); + before = ReadBack(device, block); + programCount = 0; + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 2).Bytes()); + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes()); + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled"); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + std::puts("1_5_5_5 unorm color target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f86055fbc..05e6269e1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -508,7 +508,7 @@ Throughout the project, every function at every stage either **does exactly what - [Comp-to-single DCC keys](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) is expanded only by a CB DCC decompress pass over uniform keys of a non-linear target: every texel takes the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. A fast-clear eliminate over such keys and mixed keys throw; textures and color targets that read them use their stored texels. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. -- [1_5_5_5 color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested; no draw into the target is executed in a test, and the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. +- [1_5_5_5 color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [COPY_DATA from the GPU clock](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [16-bit RGBA color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). The reverse orders (`COMP_SWAP` 2 and 3) keep throwing when blending, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. - [UINT16_ABGR color exports](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws From 21b0eb7fae1efece539cc22da476c25b8f96539e Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:31:08 +0530 Subject: [PATCH 240/318] refactor(libkernel): accept memory type in PoolCommit helper signature --- core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp index 0aed1836e..2071ad9a3 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp @@ -23,7 +23,8 @@ PoolState& Pool() { bool PageAligned(uint64_t v) { return (v & (PS5_PAGE_SIZE - 1)) == 0; } -int PoolCommit(void* addr, uint64_t len, int prot) { +int PoolCommit(void* addr, uint64_t len, int type, int prot) { + (void)type; if (!addr || len == 0 || !PageAligned(reinterpret_cast(addr)) || !PageAligned(len)) return SCE_KERNEL_ERROR_EINVAL; const int ret = DoMprotect(addr, static_cast(len), prot); if (ret == 0) { @@ -67,7 +68,7 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry* entries const KernelMemoryPoolBatchEntry& e = entries[done]; ret = Guarded([&] { switch (e.op) { - case 1: return PoolCommit(e.commit.addr, e.commit.len, e.commit.prot); + case 1: return PoolCommit(e.commit.addr, e.commit.len, 0, e.commit.prot); case 2: return PoolDecommit(e.decommit.addr, e.decommit.len); case 3: return DoMprotect(e.protect.addr, static_cast(e.protect.len), e.protect.prot); case 4: return DoMtypeprotect(e.type_protect.addr, static_cast(e.type_protect.len), e.type_protect.type, e.type_protect.prot); @@ -83,7 +84,7 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry* entries int APS5_VABI sceKernelMemoryPoolCommit(void* addr, size_t len, int type, int prot, int flags) { (void)type; (void)flags; - return Guarded([&] { return PoolCommit(addr, len, prot); }); + return Guarded([&] { return PoolCommit(addr, len, 0, prot); }); } int APS5_VABI sceKernelMemoryPoolDecommit(void* addr, size_t len, int flags) { From 337db209faf81d14cef329d92afb1cd03f2502b2 Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:31:25 +0530 Subject: [PATCH 241/318] fix(libkernel): route memory type to DoMtypeprotect in PoolCommit --- core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp index 2071ad9a3..22f96838d 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp @@ -24,9 +24,8 @@ PoolState& Pool() { bool PageAligned(uint64_t v) { return (v & (PS5_PAGE_SIZE - 1)) == 0; } int PoolCommit(void* addr, uint64_t len, int type, int prot) { - (void)type; if (!addr || len == 0 || !PageAligned(reinterpret_cast(addr)) || !PageAligned(len)) return SCE_KERNEL_ERROR_EINVAL; - const int ret = DoMprotect(addr, static_cast(len), prot); + const int ret = DoMtypeprotect(addr, static_cast(len), type, prot); if (ret == 0) { PoolState& pool = Pool(); std::lock_guard lock(pool.mutex); From 7b552bf14313a50feb97748c5de21df11ac4c68e Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:31:39 +0530 Subject: [PATCH 242/318] fix(libkernel): forward type parameter from sceKernelMemoryPoolCommit --- core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp index 22f96838d..70120567e 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp @@ -81,9 +81,8 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry* entries } int APS5_VABI sceKernelMemoryPoolCommit(void* addr, size_t len, int type, int prot, int flags) { - (void)type; (void)flags; - return Guarded([&] { return PoolCommit(addr, len, 0, prot); }); + return Guarded([&] { return PoolCommit(addr, len, type, prot); }); } int APS5_VABI sceKernelMemoryPoolDecommit(void* addr, size_t len, int flags) { From aeb131a5b776f9932f0b30b293e56714011b269b Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:31:52 +0530 Subject: [PATCH 243/318] fix(libkernel): forward entry type in sceKernelMemoryPoolBatch op 1 --- core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp index 70120567e..7f15f884a 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp @@ -67,7 +67,7 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry* entries const KernelMemoryPoolBatchEntry& e = entries[done]; ret = Guarded([&] { switch (e.op) { - case 1: return PoolCommit(e.commit.addr, e.commit.len, 0, e.commit.prot); + case 1: return PoolCommit(e.commit.addr, e.commit.len, e.commit.type, e.commit.prot); case 2: return PoolDecommit(e.decommit.addr, e.decommit.len); case 3: return DoMprotect(e.protect.addr, static_cast(e.protect.len), e.protect.prot); case 4: return DoMtypeprotect(e.type_protect.addr, static_cast(e.type_protect.len), e.type_protect.type, e.type_protect.prot); From 899e7ddd0ac96338217f7f77011bc441882950f1 Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:32:07 +0530 Subject: [PATCH 244/318] test(libkernel): declare sceKernelMemoryPoolCommit in guest memory test harness --- core/libs/tests/GuestMemory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index d1fafa044..4d10a4c38 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -64,6 +64,7 @@ int APS5_VABI sceKernelAvailableDirectMemorySize(std::int64_t, std::int64_t, std int APS5_VABI sceKernelCheckedReleaseDirectMemory(std::int64_t, std::size_t); int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); int APS5_VABI sceKernelMemoryPoolReserve(void*, std::size_t, std::size_t, int, void**); +int APS5_VABI sceKernelMemoryPoolCommit(void*, std::size_t, int, int, int); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); From e04347ca60b52f7ad3d792597c133fcf6402d5ef Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:32:24 +0530 Subject: [PATCH 245/318] test(libkernel): call sceKernelMemoryPoolCommit in direct memory test --- core/libs/tests/GuestMemory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 4d10a4c38..da2b3d2db 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -448,6 +448,7 @@ static void CheckMtypeprotect() { Require(sceKernelMemoryPoolBatch(&poolEntry, 1, &processed, 0) == 0 && processed == 1); check(1, 3, 1); check(2, 4, 1); + Require(sceKernelMemoryPoolCommit(bytes + page * 2, page, 6, 3, 0) == 0); Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); VirtualQueryInfo whole{}; Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); From f608965b47d3b3557a35d9ee33fefa099e3864fa Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:32:37 +0530 Subject: [PATCH 246/318] test(libkernel): assert direct memory type update after pool commit --- core/libs/tests/GuestMemory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index da2b3d2db..1b5996d6b 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -449,6 +449,7 @@ static void CheckMtypeprotect() { check(1, 3, 1); check(2, 4, 1); Require(sceKernelMemoryPoolCommit(bytes + page * 2, page, 6, 3, 0) == 0); + check(2, 6, 3); Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); VirtualQueryInfo whole{}; Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); From e106b3ba9b1cbdb6c6a7928598b0bf3bce5fec0a Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:32:55 +0530 Subject: [PATCH 247/318] test(libkernel): assert neighbor page isolation after pool commit --- core/libs/tests/GuestMemory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 1b5996d6b..8da05b87e 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -450,6 +450,7 @@ static void CheckMtypeprotect() { check(2, 4, 1); Require(sceKernelMemoryPoolCommit(bytes + page * 2, page, 6, 3, 0) == 0); check(2, 6, 3); + check(1, 3, 1); Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); VirtualQueryInfo whole{}; Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); From ed3ca36b54fa02ab9ce0f88426540fb94ca403d3 Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:33:19 +0530 Subject: [PATCH 248/318] test(libkernel): construct batch commit entry for memory pool test --- core/libs/tests/GuestMemory.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 8da05b87e..0dad49c6f 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -451,6 +451,12 @@ static void CheckMtypeprotect() { Require(sceKernelMemoryPoolCommit(bytes + page * 2, page, 6, 3, 0) == 0); check(2, 6, 3); check(1, 3, 1); + KernelMemoryPoolBatchEntry commitEntry{}; + commitEntry.op = 1; + commitEntry.commit.addr = bytes + page * 2; + commitEntry.commit.len = page; + commitEntry.commit.prot = 1; + commitEntry.commit.type = 7; Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); VirtualQueryInfo whole{}; Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); From 4f947dab167904649a5e3576c88da76d6ce925ae Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:33:47 +0530 Subject: [PATCH 249/318] test(libkernel): dispatch memory pool batch op 1 commit entry in test --- core/libs/tests/GuestMemory.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 0dad49c6f..6cd0f1fff 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -457,6 +457,8 @@ static void CheckMtypeprotect() { commitEntry.commit.len = page; commitEntry.commit.prot = 1; commitEntry.commit.type = 7; + processed = -1; + Require(sceKernelMemoryPoolBatch(&commitEntry, 1, &processed, 0) == 0 && processed == 1); Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); VirtualQueryInfo whole{}; Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); From 99a39859db7f2f184a3229c3fb0e4bef9204bfc7 Mon Sep 17 00:00:00 2001 From: abhinav29102005 Date: Mon, 12 Oct 2026 11:34:03 +0530 Subject: [PATCH 250/318] test(libkernel): assert memory type propagation from batch commit entry --- core/libs/tests/GuestMemory.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 6cd0f1fff..2a88211b9 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -459,6 +459,8 @@ static void CheckMtypeprotect() { commitEntry.commit.type = 7; processed = -1; Require(sceKernelMemoryPoolBatch(&commitEntry, 1, &processed, 0) == 0 && processed == 1); + check(1, 3, 1); + check(2, 7, 1); Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); VirtualQueryInfo whole{}; Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); From fdfbeba8addb07d212e904acc963eb98ad25e0d3 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 16:09:23 +1300 Subject: [PATCH 251/318] fix(agc): accept MSAA_ENABLE on a single-sample target sampled at the pixel center --- .../libSceAgcDriver/Graphics/src/State.cpp | 41 +++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 68 +++++++++ .../execution/MsaaEnableSingleSample.cpp | 132 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index ce50da558..b439c64a4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -317,6 +317,36 @@ constexpr std::array, 2> StandardLocations2{{{4, 4}, constexpr std::array, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}}; constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -7}}}; +std::string singleSampleMsaaRejection(const QueueState& queue) { + const auto& cx = queue.context; + const auto word = [](const Registers& registers, std::uint32_t offset, RegisterBank bank = RegisterBank::Context) -> std::optional { + const auto it = find(registers, offset, bank); + if (it == registers.end()) return std::nullopt; + return it->second; + }; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + const auto offset = 0x2feu + pixel * 4u; + if (const auto locations = word(cx, offset); locations && (*locations & 0xffu) != 0) return zeroMessage(offset, *locations, "MSAA_ENABLE on a single-sample target whose sample is off the pixel center"); + } + if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0) return zeroMessage(0x2dc, *alphaToMask, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + if (const auto conservative = word(cx, 0x313); conservative && (*conservative & ~0x001f6000u) != 0) return zeroMessage(0x313, *conservative, "MSAA_ENABLE on a single-sample target with conservative rasterization"); + constexpr const char* primitives = "MSAA_ENABLE on a single-sample target for primitives other than triangles"; + const auto stages = word(cx, 0x2d5); + if (!stages) return {}; + if ((*stages & 0x20u) != 0) { + if (const auto output = word(cx, 0x29b); output && ((*output & 0x80000000u) != 0 || (*output & 0x3fu) != 2u)) return zeroMessage(0x29b, *output, primitives); + } else if ((*stages & 4u) != 0) { + if (const auto parameters = word(cx, 0x2db)) { + const auto domain = *parameters & 3u; + const auto topology = (*parameters >> 5u) & 7u; + if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, primitives); + } + } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u) { + return zeroMessage(0x242, *primitive, primitives); + } + return {}; +} + std::uint32_t decodeSamples(const Registers& cx) { const auto config = read(cx, 0x2f8); zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); @@ -690,8 +720,12 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.alphaToCoverage = read(cx, 0x2dc) == AlphaToCoverageUndithered; if (!result.alphaToCoverage) zero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); - zero(cx, 0x292, result.samples > 1 ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); + zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported"); + if (result.samples == 1 && (read(cx, 0x292) & 1u) != 0) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) static_cast(read(cx, 0x2feu + pixel * 4u)); + if (auto reason = singleSampleMsaaRejection(queue); !reason.empty()) throw std::runtime_error(reason); + } zero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); zero(cx, 0x80, ~0u, "window offset"); zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); @@ -1142,7 +1176,10 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { } if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0; - if (auto reason = nonzero(cx, 0x292, multisampled ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); !reason.empty()) return reason; + if (!multisampled && value(cx, 0x292, word) && (word & 1u) != 0) { + if (auto reason = singleSampleMsaaRejection(queue); !reason.empty()) return reason; + } if (auto reason = nonzero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x80, ~0u, "window offset"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 39c8dd8b2..cbdaa2f9e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -450,6 +450,73 @@ void hardwareScreenOffsetTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void MsaaEnableSingleSampleTests() { + using AgcDriver::Graphics::DecodeState; + using AgcDriver::Graphics::DrawRejection; + const auto enabled = [] { + auto queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0; + return queue; + }; + const auto refused = [](const AgcDriver::QueueState& queue, std::string_view reason) { + expectFailure([&] { DecodeState(queue); }, reason); + const auto rejection = DrawRejection(queue, false); + Require(rejection.find(reason) != std::string::npos, "the precheck did not refuse MSAA_ENABLE on a single-sample target (" + std::string(reason) + "): " + rejection); + }; + auto queue = enabled(); + auto clear = queue; + clear.context[0x292] = 2; + const auto baseline = DecodeState(clear); + for (const auto locations : {0x00000000u, 0xcc44cc00u}) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = locations; + const auto state = DecodeState(queue); + Require(state.samples == 1 && state.colors.size() == 1 && state.colors[0].samples == 1 && !state.alphaToCoverage, "MSAA_ENABLE on a single-sample target did not decode as single-sampled"); + Require(state.topology == baseline.topology && state.conservativeRasterization == baseline.conservativeRasterization && state.cullMode == baseline.cullMode && state.frontFace == baseline.frontFace, "MSAA_ENABLE on a single-sample target changed the rasterizer state"); + Require(state.scissor.offset.x == baseline.scissor.offset.x && state.scissor.offset.y == baseline.scissor.offset.y && state.scissor.extent.width == baseline.scissor.extent.width && state.scissor.extent.height == baseline.scissor.extent.height, "MSAA_ENABLE on a single-sample target changed the scissor"); + Require(state.colors[0].address == baseline.colors[0].address && state.colors[0].format == baseline.colors[0].format && state.blend.colorWriteMask == baseline.blend.colorWriteMask, "MSAA_ENABLE on a single-sample target changed the color target"); + const auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused MSAA_ENABLE on a single-sample target: " + rejection); + } + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + for (const auto location : {0x04u, 0x40u, 0xc4u}) { + queue = enabled(); + queue.context[0x2fe + pixel * 4u] = location; + refused(queue, "off the pixel center"); + } + } + queue = enabled(); + queue.context.erase(0x306); + expectFailure([&] { DecodeState(queue); }, "missing register"); + queue = enabled(); + queue.context[0x2dc] = 0xaa01; + refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + queue = enabled(); + queue.context[0x313] = 0x6001; + refused(queue, "MSAA_ENABLE on a single-sample target with conservative rasterization"); + for (const auto primitive : {2u, 7u, 17u}) { + queue = enabled(); + queue.userConfig[0x242] = primitive; + refused(queue, "primitives other than triangles"); + } + queue = enabled(); + queue.userConfig[0x242] = 1; + Require(DrawRejection(queue, false).find("primitives other than triangles") != std::string::npos, "the precheck accepted MSAA_ENABLE on a single-sample target for points"); + for (const auto primitive : {5u, 6u}) { + queue = enabled(); + queue.userConfig[0x242] = primitive; + Require(DecodeState(queue).samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target was refused for a triangle fan or strip"); + } + queue = enabled(); + queue.context[0x292] = 7; + refused(queue, "scan conversion mode"); + clear.context[0x2fe] = 0x44; + clear.context[0x2dc] = 0xaa01; + Require(DecodeState(clear).alphaToCoverage && DrawRejection(clear, false).empty(), "a single-sample target without MSAA_ENABLE was refused over its sample locations or alpha-to-coverage"); +} + void ShaderStageTests() { auto queue = makeState(); for (const auto routing : {0x2000u, 0x2010u, 0x02002000u, 0x02002010u}) { @@ -4719,6 +4786,7 @@ int main(int argc, char** argv) { uint16ExportTests(); uint8x4TargetTests(); multisampleTests(); + MsaaEnableSingleSampleTests(); ShaderStageTests(); NggVertexWorkgroupTests(); TuningFieldTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp new file mode 100644 index 000000000..d7500ebc2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp @@ -0,0 +1,132 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array ClearMemory{}; +alignas(256) std::array EnabledMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawBoth() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto draw = [&](std::array& memory, std::uint32_t scanMode) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x69, {0x292, scanMode}); + Append(commands, 0x2d, {3, 2}); + }; + draw(ClearMemory, 2); + draw(EnabledMemory, 3); + return commands; +} + +void MsaaEnableDrawsLikeDisabled() { + ClearMemory.fill(Kept); + EnabledMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawBoth(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto kept = static_cast(std::count(ClearMemory.begin(), ClearMemory.end(), Kept)); + Require(kept != 0 && kept != ClearMemory.size(), "the triangle without MSAA_ENABLE covered " + std::to_string(ClearMemory.size() - kept) + " of " + std::to_string(ClearMemory.size()) + " bytes, expected an edge through the target"); + for (std::size_t index = 0; index < ClearMemory.size(); ++index) { + Require(EnabledMemory[index] == ClearMemory[index], "byte " + std::to_string(index) + " is " + std::to_string(static_cast(EnabledMemory[index])) + " with MSAA_ENABLE on a single-sample target and " + std::to_string(static_cast(ClearMemory[index])) + " without it"); + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + MsaaEnableDrawsLikeDisabled(); + AgcDriverShutdown_nid_postfix(); + std::puts("MSAA_ENABLE on a single-sample target drew the same pixels as the state without it"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 672d11da6..7c50a71a9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -151,6 +151,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceGameLiveStreamingGetCurrentStatus2](../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id - [Depth clear draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [Alpha-to-coverage](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw +- [MSAA_ENABLE on a single-sample target](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. Other locations of sample 0, absent location registers, points, lines, rectangles, alpha-to-coverage and conservative rasterization throw - [GPU flips](../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 - [sceAgcDriverGetEqContextId](../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted From fd0d55ce2100a7e651b269797c32ed10a37c113a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 06:10:56 +0000 Subject: [PATCH 252/318] fix(tools): follow Depends on chains in pr_overlap pr_overlap only skipped a pair when one pull request named the other directly. In a stack where #3 depends on #2 and #2 on #1, #1 and #3 were reported as overlapping, and two pull requests built on the same #1 were reported for the files and exports both inherit from it. Dependencies are now followed through open pull requests, and what a shared dependency adds is left out of the same-file and same-export columns. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0189deyapHNNqKQJboZBJQei --- tools/pr_overlap.py | 18 +++++++-- tools/tests/test_pr_overlap_dependencies.py | 42 +++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 279006dba..e1497d7f7 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -115,14 +115,26 @@ def conflicts(a, b): return {path: [] if path in a["added"] else clashes(a, b, path) for path in files} +def dependencies(prs, number): + found, pending = set(), [number] + while pending: + for n in prs[pending.pop()]["depends"]: + if n in prs and n not in found: + found.add(n) + pending.append(n) + return found + + def overlaps(prs, a, b): pa, pb = prs[a], prs[b] - if b in pa["depends"] or a in pb["depends"]: + da, db = dependencies(prs, a), dependencies(prs, b) + if b in da or a in db: return None - found = {"added": pa["added"] & pb["added"]} + shared = [prs[n] for n in da & db] + found = {"added": (pa["added"] & pb["added"]).difference(*(pr["added"] for pr in shared))} if pa["author"] != pb["author"]: found["conflicts"] = conflicts(pa, pb) - found["exports"] = pa["exports"] & pb["exports"] + found["exports"] = (pa["exports"] & pb["exports"]).difference(*(pr["exports"] for pr in shared)) return found if any(found.values()) else None diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index f93bb828c..b8590858f 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -16,6 +16,23 @@ def pull_requests(body): return pr_overlap.open_prs("main") +STACK = { + 1: (set(), {"one.cpp"}, {"sceFooInit"}), + 2: ({1, 999}, {"one.cpp", "two.cpp"}, {"sceFooInit"}), + 3: ({2}, {"one.cpp", "two.cpp", "three.cpp"}, {"sceFooInit"}), + 4: ({1}, {"one.cpp", "four.cpp"}, {"sceFooInit", "sceFooTerm"}), + 5: ({1}, {"one.cpp", "four.cpp"}, {"sceFooInit", "sceFooTerm"}), + 6: (set(), {"one.cpp"}, {"sceFooInit"}), + 7: ({8}, {"one.cpp", "seven.cpp"}, {"sceFooInit"}), + 8: ({7, 1}, {"one.cpp", "seven.cpp"}, {"sceFooInit"}), +} + + +def stacked_pull_requests(): + return {number: {"author": f"author{number}", "depends": depends, "added": added, "exports": exports, "tree": None} + for number, (depends, added, exports) in STACK.items()} + + class DependencyTests(unittest.TestCase): def test_documented_form_without_colon(self): for body in ("Depends on #2.", "Depends on #2, #3.", "Depends on\t#2 and #3.", @@ -95,5 +112,30 @@ class DependencyTests(unittest.TestCase): self.assertEqual(pr_overlap.overlaps(prs, 1, 2), {"added": {"shared.cpp"}}) +class StackedDependencyTests(unittest.TestCase): + def test_transitive_dependency_suppresses_overlap(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 1, 3)) + self.assertIsNone(pr_overlap.overlaps(prs, 3, 1)) + + def test_changes_inherited_from_a_shared_dependency_are_not_overlap(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 3, 4)) + self.assertIsNone(pr_overlap.overlaps(prs, 2, 4)) + + def test_overlap_beyond_a_shared_dependency_is_kept(self): + self.assertEqual(pr_overlap.overlaps(stacked_pull_requests(), 4, 5), + {"added": {"four.cpp"}, "conflicts": {}, "exports": {"sceFooTerm"}}) + + def test_independent_pull_request_keeps_inherited_overlap(self): + self.assertEqual(pr_overlap.overlaps(stacked_pull_requests(), 3, 6), + {"added": {"one.cpp"}, "conflicts": {}, "exports": {"sceFooInit"}}) + + def test_dependency_cycle_terminates(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 7, 8)) + self.assertIsNone(pr_overlap.overlaps(prs, 8, 2)) + + if __name__ == "__main__": unittest.main() From c914bcd72138c79feb75014b5177ff3ff1d9efee Mon Sep 17 00:00:00 2001 From: Avijit Singh Date: Mon, 12 Oct 2026 06:10:07 +0530 Subject: [PATCH 253/318] feat(agc): describe color mip targets with parent layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Graphics/include/ColorTargetLayout.hpp | 3 +++ .../Graphics/src/ColorTargetLayout.cpp | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp index 575de2335..64ea197ff 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_COLORTARGETLAYOUT_HPP #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include #include #include @@ -33,6 +34,8 @@ constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) { } ColorTileMode DecodeColorTileMode(std::uint32_t attrib3); +// Layout for one color mip addressed as a separate target buffer, retaining its packed-tail origin. +TileMipLayout ComputeColorTargetMipLayout(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t mipCount, std::uint32_t mipLevel); class ColorTargetLayout { public: diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index c406e4b6c..832305195 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include #include @@ -52,6 +53,19 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) { return static_cast(mode); } +TileMipLayout ComputeColorTargetMipLayout(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t mipCount, std::uint32_t mipLevel) { + auto mips = ComputeElementMipLayout(ColorTextureTileMode(mode), bytesPerElement, surfaceWidth, surfaceHeight, mipCount); + require(mipLevel < mips.size(), "AGC graphics: color target mip is outside its surface"); + auto mip = mips[mipLevel]; + const ColorTargetLayout target(mip.width, mip.height, mode, bytesPerElement); + require(mip.tiledSize == target.Bytes(), "AGC graphics: color target mip tiling differs from its surface layout"); + mip.tiledOffset = 0; + mip.linearOffset = 0; + mip.pitchBytes = mip.width * bytesPerElement; + mip.linearSize = target.LinearBytes(); + return mip; +} + namespace { struct SwizzleTables { From c58c9488e5d3435bd35cd6780f6348a5435b0479 Mon Sep 17 00:00:00 2001 From: Avijit Singh Date: Mon, 12 Oct 2026 06:10:13 +0530 Subject: [PATCH 254/318] test(agc): cover packed color mip detiling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../libSceAgcDriver/tests/TextureDetiler.cpp | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp index 1b9c4784e..e1f50d866 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp @@ -1,4 +1,5 @@ #include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include #include @@ -66,8 +67,8 @@ void reject(TAction action, std::string_view reason) { void RunTextureDetilerTests(const Context& context, const TextureDetilerTestAccess& access) { TextureDetiler detiler(context); const auto commands = reinterpret_cast(static_cast(1)); - const auto source = access.makeBuffer(4096); - const auto destination = access.makeBuffer(4096); + const auto source = access.makeBuffer(65536); + const auto destination = access.makeBuffer(65536); const auto layout = makeLayout(20, 12, 96, 5, 64, 48); detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 20, destination, 40, layout, 0); @@ -171,6 +172,31 @@ void RunTextureDetilerTests(const Context& context, const TextureDetilerTestAcce Require(decodePush(access.lastDispatch().pushConstants).pipeBankXor == 0xa00u, "the window's pipe/bank XOR did not reach the detiling shader"); Require(equationSpecialization[0] == 4 && equationSpecialization[1] == 4096 && equationSpecialization[2] == 2, "SW_4KB_D_X detiling must select the equation family over 4 KiB blocks"); + { + const auto mip = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 4); + Require(mip.width == 64 && mip.height == 64 && mip.tail, "the first packed render-target mip has the wrong extent"); + Require(mip.tiledOffset == 0 && mip.tiledSize == 65536 && mip.tailX == 64 && mip.tailY == 0, "the first packed render-target mip lost its shared-block origin"); + Require(mip.pitchBytes == 256 && mip.linearSize == 16384, "packed render-target mips must use tightly packed transfer rows"); + const auto next = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 5); + Require(next.width == 32 && next.height == 32 && next.tail && next.tiledOffset == 0 && next.tiledSize == 65536 && next.tailX == 0 && next.tailY == 64, "the next packed render-target mip lost its shared-block origin"); + const auto regular = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 3); + Require(!regular.tail && regular.width == 128 && regular.height == 128 && regular.tiledOffset == 0 && regular.tiledSize == 65536, "a regular render-target mip must be relative to its own target buffer"); + reject([] { ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 11); }, "outside its surface"); + + const auto tileMode = ColorTextureTileMode(ColorTileMode::RenderTarget); + detiler.Dispatch(commands, tileMode, 4, source, 0, destination, 0, mip); + capture = access.lastDispatch(); + push = decodePush(capture.pushConstants); + Require(push.width == 64 && push.height == 64 && push.tail == 1 && push.tailX == 64 && push.tailY == 0, "packed mip tail coordinates did not reach the detile shader"); + Require(capture.sourceRange == 65536 && capture.destinationRange == 16384, "packed mip detile must read its shared guest block and write its tightly packed pixels"); + + detiler.Dispatch(commands, tileMode, 4, destination, 0, source, 0, mip, true); + capture = access.lastDispatch(); + push = decodePush(capture.pushConstants); + Require(push.width == 64 && push.height == 64 && push.tail == 1 && push.tailX == 64 && push.tailY == 0, "packed mip tail coordinates did not reach the retile shader"); + Require(capture.sourceRange == 16384 && capture.destinationRange == 65536, "packed mip retile must read tightly packed pixels and preserve the shared guest block"); + } + reject([&] { detiler.Dispatch(VK_NULL_HANDLE, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, layout, 0); }, "active command buffer"); reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, VK_NULL_HANDLE, 0, destination, 0, layout, 0); }, "source and destination buffers"); reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, VK_NULL_HANDLE, 0, layout, 0); }, "source and destination buffers"); From 89c128782c3fcdbf0b1a01db8ed11728d54d5383 Mon Sep 17 00:00:00 2001 From: Avijit Singh Date: Mon, 12 Oct 2026 06:10:21 +0530 Subject: [PATCH 255/318] fix(agc): render nonresident packed mip targets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 2f3bf711d..13b48723a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -361,18 +361,6 @@ void memoryBarrier(const Context& context, VkCommandBuffer commands, VkPipelineS context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 1, &barrier, 0, nullptr, 0, nullptr); } -// The color surface as one untailed SW_64KB_R_X mip for the GPU detiler, with tightly packed linear rows. -TileMipLayout ColorTargetMip(const ColorTarget& color, const ColorTargetLayout& layout) { - TileMipLayout mip{}; - mip.width = color.extent.width; - mip.height = color.extent.height; - mip.blocksPerRow = layout.BlocksPerRow(); - mip.pitchBytes = color.extent.width * color.elementBytes; - mip.tiledSize = layout.Bytes(); - mip.linearSize = layout.LinearBytes(); - return mip; -} - // APS5_PROFILE_DRAW: per-draw phase timers in microseconds (a recorded draw's phases are far below // the millisecond the old print rounded to), totalled over 10 s in the [draws] line. enum DrawPhase : std::size_t { PhaseValidate, PhaseVertex, PhaseSetup, PhaseReadTarget, PhasePrepare, PhaseLookup, PhaseResources, PhasePipeline, PhaseRecord, PhaseKeep, PhaseSync, PhaseWriteBack, PhaseDescribe, PhaseCount }; @@ -1949,11 +1937,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& continue; } materializeCmaskClear(context, color, nullptr); - Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface"); + Require(!color.mipTail || binding.gpuTiling, "rendering into a packed mip tail requires the GPU tiled fallback"); Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface"); Require(color.arraySlice == 0 || !ColorTileModeIsXor(color.tileMode), "rendering into a slice of a 2D array color target with an XOR tile mode needs the resident image of its array"); if (binding.gpuTiling) { - binding.mip = ColorTargetMip(color, colorLayout); + binding.mip = ComputeColorTargetMipLayout(color.tileMode, color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount, color.mip); binding.tiled = std::make_unique(context, colorLayout.Bytes(), copies); binding.tiledDevice = std::make_unique(context, colorLayout.Bytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); binding.linearDevice = std::make_unique(context, colorLayout.LinearBytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); From 485c0f33467256ecf14482c53f6652d7e511617f Mon Sep 17 00:00:00 2001 From: Avijit Singh Date: Mon, 12 Oct 2026 06:10:59 +0530 Subject: [PATCH 256/318] style(agc): keep mip layout API comment-free Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp index 64ea197ff..d5a82a6ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp @@ -34,7 +34,6 @@ constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) { } ColorTileMode DecodeColorTileMode(std::uint32_t attrib3); -// Layout for one color mip addressed as a separate target buffer, retaining its packed-tail origin. TileMipLayout ComputeColorTargetMipLayout(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t mipCount, std::uint32_t mipLevel); class ColorTargetLayout { From 9394c74301de57df5ca8311b1167cffca038c7b0 Mon Sep 17 00:00:00 2001 From: Felipe Arce Date: Mon, 12 Oct 2026 03:17:31 -0300 Subject: [PATCH 257/318] fix(agc): keep untailed color targets on their own detiler layout --- .../libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 13b48723a..0b1c5bf16 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -361,6 +361,18 @@ void memoryBarrier(const Context& context, VkCommandBuffer commands, VkPipelineS context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 1, &barrier, 0, nullptr, 0, nullptr); } +// The color surface as one untailed SW_64KB_R_X mip for the GPU detiler, with tightly packed linear rows. +TileMipLayout ColorTargetMip(const ColorTarget& color, const ColorTargetLayout& layout) { + TileMipLayout mip{}; + mip.width = color.extent.width; + mip.height = color.extent.height; + mip.blocksPerRow = layout.BlocksPerRow(); + mip.pitchBytes = color.extent.width * color.elementBytes; + mip.tiledSize = layout.Bytes(); + mip.linearSize = layout.LinearBytes(); + return mip; +} + // APS5_PROFILE_DRAW: per-draw phase timers in microseconds (a recorded draw's phases are far below // the millisecond the old print rounded to), totalled over 10 s in the [draws] line. enum DrawPhase : std::size_t { PhaseValidate, PhaseVertex, PhaseSetup, PhaseReadTarget, PhasePrepare, PhaseLookup, PhaseResources, PhasePipeline, PhaseRecord, PhaseKeep, PhaseSync, PhaseWriteBack, PhaseDescribe, PhaseCount }; @@ -1941,7 +1953,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface"); Require(color.arraySlice == 0 || !ColorTileModeIsXor(color.tileMode), "rendering into a slice of a 2D array color target with an XOR tile mode needs the resident image of its array"); if (binding.gpuTiling) { - binding.mip = ComputeColorTargetMipLayout(color.tileMode, color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount, color.mip); + binding.mip = color.mipTail ? ComputeColorTargetMipLayout(color.tileMode, color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount, color.mip) : ColorTargetMip(color, colorLayout); binding.tiled = std::make_unique(context, colorLayout.Bytes(), copies); binding.tiledDevice = std::make_unique(context, colorLayout.Bytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); binding.linearDevice = std::make_unique(context, colorLayout.LinearBytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); From 6c94d31d7776b0d21116719f4ebbad3abc39ed05 Mon Sep 17 00:00:00 2001 From: Felipe Arce Date: Mon, 12 Oct 2026 03:17:31 -0300 Subject: [PATCH 258/318] test(agc): render into packed mip tails through the guest-memory fallback --- .../libSceAgcDriver/tests/TextureDetiler.cpp | 17 +- .../tests/execution/PackedMipTailTarget.cpp | 257 ++++++++++++++++++ 2 files changed, 259 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedMipTailTarget.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp index e1f50d866..fe008dfb1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp @@ -67,8 +67,8 @@ void reject(TAction action, std::string_view reason) { void RunTextureDetilerTests(const Context& context, const TextureDetilerTestAccess& access) { TextureDetiler detiler(context); const auto commands = reinterpret_cast(static_cast(1)); - const auto source = access.makeBuffer(65536); - const auto destination = access.makeBuffer(65536); + const auto source = access.makeBuffer(4096); + const auto destination = access.makeBuffer(4096); const auto layout = makeLayout(20, 12, 96, 5, 64, 48); detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 20, destination, 40, layout, 0); @@ -182,19 +182,6 @@ void RunTextureDetilerTests(const Context& context, const TextureDetilerTestAcce const auto regular = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 3); Require(!regular.tail && regular.width == 128 && regular.height == 128 && regular.tiledOffset == 0 && regular.tiledSize == 65536, "a regular render-target mip must be relative to its own target buffer"); reject([] { ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 11); }, "outside its surface"); - - const auto tileMode = ColorTextureTileMode(ColorTileMode::RenderTarget); - detiler.Dispatch(commands, tileMode, 4, source, 0, destination, 0, mip); - capture = access.lastDispatch(); - push = decodePush(capture.pushConstants); - Require(push.width == 64 && push.height == 64 && push.tail == 1 && push.tailX == 64 && push.tailY == 0, "packed mip tail coordinates did not reach the detile shader"); - Require(capture.sourceRange == 65536 && capture.destinationRange == 16384, "packed mip detile must read its shared guest block and write its tightly packed pixels"); - - detiler.Dispatch(commands, tileMode, 4, destination, 0, source, 0, mip, true); - capture = access.lastDispatch(); - push = decodePush(capture.pushConstants); - Require(push.width == 64 && push.height == 64 && push.tail == 1 && push.tailX == 64 && push.tailY == 0, "packed mip tail coordinates did not reach the retile shader"); - Require(capture.sourceRange == 16384 && capture.destinationRange == 65536, "packed mip retile must read tightly packed pixels and preserve the shared guest block"); } reject([&] { detiler.Dispatch(VK_NULL_HANDLE, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, layout, 0); }, "active command buffer"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedMipTailTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedMipTailTarget.cpp new file mode 100644 index 000000000..a8a85b621 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedMipTailTarget.cpp @@ -0,0 +1,257 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using AgcDriver::Graphics::TextureTileMode; +using AgcDriver::Graphics::TileMipLayout; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Levels = 9; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t BlockSide = 128; +constexpr std::size_t SurfaceBytes = BlockBytes + Side * Side * 4u + BlockBytes; +constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Initial = 0x5a5a5a5au; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, Levels> PixelCodes{}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + BlockBytes) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + BlockBytes - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "packed mip tail target: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t level, const TileMipLayout& mip) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = level << 26u; + cx[0x31c] = Color32FloatInfo; + cx[0x31d] = 0; + cx[0x3b0] = ((Levels - 1u) << 28u) | ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R32_SFLOAT && color.tileMode == ColorTileMode::RenderTarget, "the COLOR_32 float view did not decode as an SW_64KB_R_X R32 target"); + Require(color.mip == level && color.mipCount == Levels && color.mipTail == mip.tail, "the color view did not keep its mip of the surface"); + Require(color.extent.width == mip.width && color.extent.height == mip.height && color.address == address + mip.tiledOffset, "the color view did not decode the extent and address of mip " + std::to_string(level)); + return color; +} + +void DrawLevel(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t level, const TileMipLayout& mip, std::uint32_t value) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + auto& pixelWords = PixelCodes[level]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + pixelWords[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + pixelWords[at++] = value; + } + pixelWords[at++] = 0xf800180fu; + pixelWords[at++] = 0x07060504u; + pixelWords[at++] = 0xbf810000u; + const std::span pixelCode(pixelWords.data(), at); + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + const auto color = DecodeTarget(surface, level, mip); + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = color.extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(mip.height), static_cast(mip.width), -static_cast(mip.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, color.extent}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Expect(std::vector& expected, const TileMipLayout& mip, std::uint32_t value) { + const ColorTargetLayout block(BlockSide, BlockSide, ColorTileMode::RenderTarget, 4); + const ColorTargetLayout own(mip.width, mip.height, ColorTileMode::RenderTarget, 4); + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const auto offset = mip.tail ? block.Offset(x + mip.tailX, y + mip.tailY) : mip.tiledOffset + own.Offset(x, y); + Require(expected.at(offset / 4u) == Initial, "the expected texels of two mips overlap at byte " + Hex(offset)); + expected[offset / 4u] = value; + } + } +} + +void Check(const std::uint8_t* texels, const std::vector& expected, const std::string& what) { + const auto* words = reinterpret_cast(texels); + for (std::size_t index = 0; index < expected.size(); ++index) { + Require(words[index] == expected[index], what + ": surface byte " + Hex(index * 4u) + " holds " + Hex(words[index]) + ", expected " + Hex(expected[index])); + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(TextureTileMode::kR64KBX, 4, Side, Side, Levels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) == SurfaceBytes, "the 256x256 R32 surface with 9 mips is not one tail block, mip 0 and mip 1"); + Require(!mips[1].tail && mips[2].tail && mips[3].tail && mips[2].width == 64 && mips[3].width == 32, "mips 2 and 3 of the 256x256 R32 surface are not its first packed tail mips"); + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + std::vector expected(SurfaceBytes / 4u, Initial); + const std::array, 3> draws{{{2u, 0.25f}, {3u, 0.75f}, {1u, 0.5f}}}; + for (const auto& [level, drawn] : draws) { + const auto value = std::bit_cast(drawn); + DrawLevel(device, surface, level, mips[level], value); + Require(StorageTexture::FindPending(surface, SurfaceBytes) == nullptr, "the draw into mip " + std::to_string(level) + " rendered into a resident image instead of the guest-memory fallback"); + Expect(expected, mips[level], value); + Check(texels, expected, "after the draw into mip " + std::to_string(level)); + } + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_NO_RESIDENT_TARGETS", "1"); +#else + setenv("APS5_NO_RESIDENT_TARGETS", "1", 1); +#endif + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, block.Data()); + std::puts("packed mip tail target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 4dfe0526b82498ef83b23bfa7c3963e43b4b7531 Mon Sep 17 00:00:00 2001 From: Felipe Arce Date: Mon, 12 Oct 2026 03:26:02 -0300 Subject: [PATCH 259/318] fix(agc): link the texture mip layout into the BDA device tests --- core/libs/prx/libSceAgcDriver/PostCompile.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/PostCompile.cmake b/core/libs/prx/libSceAgcDriver/PostCompile.cmake index 3cabf5ccd..761824014 100644 --- a/core/libs/prx/libSceAgcDriver/PostCompile.cmake +++ b/core/libs/prx/libSceAgcDriver/PostCompile.cmake @@ -28,7 +28,7 @@ foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_gra endif() endforeach() -target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp Graphics/src/ColorTargetLayout.cpp Execution/src/GuestMemory.cpp) +target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/TextureTiling.cpp Graphics/src/TextureFormat.cpp Execution/src/GuestMemory.cpp) target_include_directories(agc_driver_bda_device_tests PRIVATE ${agcTextureDetileGeneratedRoot}) if(TARGET agc_driver_visual_test) From 21b9e2efeba0b67f49180a0f0809bc1653bca98d Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:22:43 +0200 Subject: [PATCH 260/318] feat(agc): select the swapchain present mode with ANYPS5_PRESENT_MODE --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++- .../Execution/include/PresentMode.hpp | 27 +++++++++++++++++ .../Execution/src/VulkanDevice.cpp | 29 +++++++++++++++++-- core/libs/prx/libSceAgcDriver/tests/Flip.cpp | 25 ++++++++++++++++ docs/user/USAGE.md | 2 ++ 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6c94ffc4b..2fb4b8db3 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -657,6 +657,7 @@ add_test_executable(agc_driver_flip_tests EXCLUDE_FROM_ALL target_include_directories(agc_driver_flip_tests PRIVATE ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include ) target_link_libraries(agc_driver_flip_tests PRIVATE @@ -1153,10 +1154,11 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_pm4_priority_refused PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_THREAD_PRIORITY=high;DBUS_SYSTEM_BUS_ADDRESS=unix:path=/nonexistent/aps5-no-rtkit" SKIP_RETURN_CODE 77) endif() + add_test(NAME agc_driver_present_mode COMMAND agc_driver_flip_tests present-mode) add_test(NAME agc_driver_flip_compute COMMAND agc_driver_flip_tests compute) set_tests_properties(agc_driver_flip_compute PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_flip_compute PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_flip_compute agc_driver_present_mode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_draw_cache_reuse_off COMMAND agc_driver_draw_cache_reuse_tests) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp new file mode 100644 index 000000000..cb448bbc4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTMODE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTMODE_HPP + +#include +#include +#include +#include +#include + +namespace AgcDriver { + +inline VkPresentModeKHR SelectPresentMode(const char* requested, std::span supported) { + VkPresentModeKHR mode = VK_PRESENT_MODE_FIFO_KHR; + if (requested != nullptr) { + const std::string_view name(requested); + if (name == "mailbox") mode = VK_PRESENT_MODE_MAILBOX_KHR; + else if (name == "immediate") mode = VK_PRESENT_MODE_IMMEDIATE_KHR; + else if (name == "fifo_latest_ready") mode = VK_PRESENT_MODE_FIFO_LATEST_READY_KHR; + else if (name != "fifo") throw std::runtime_error("Vulkan presentation: ANYPS5_PRESENT_MODE must be fifo, fifo_latest_ready, mailbox or immediate"); + } + if (std::find(supported.begin(), supported.end(), mode) == supported.end()) throw std::runtime_error("Vulkan presentation: the surface does not support the requested present mode"); + return mode; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index d24dbbc56..072ad4c5f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -13,6 +13,7 @@ #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" @@ -241,6 +242,7 @@ struct VulkanDevice::State { VkExtent2D extent{}; std::vector images; VkFence acquireFence = VK_NULL_HANDLE; + VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR; struct RetiredSwapchain { VkSwapchainKHR swapchain = VK_NULL_HANDLE; std::vector rendered; @@ -1167,6 +1169,16 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } if (state->sampleLocationSampleCounts != 0) deviceExtensions.push_back(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME); } + const char* fifoLatestReadyExtension = hasExtension(VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME) ? VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME : hasExtension("VK_EXT_present_mode_fifo_latest_ready") ? "VK_EXT_present_mode_fifo_latest_ready" : nullptr; + const char* requestedPresentMode = std::getenv("ANYPS5_PRESENT_MODE"); + VkPhysicalDevicePresentModeFifoLatestReadyFeaturesKHR fifoLatestReadyFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_MODE_FIFO_LATEST_READY_FEATURES_KHR}; + bool fifoLatestReady = false; + if (requestedPresentMode != nullptr && std::strcmp(requestedPresentMode, "fifo_latest_ready") == 0 && fifoLatestReadyExtension != nullptr) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &fifoLatestReadyFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + fifoLatestReady = fifoLatestReadyFeatures.presentModeFifoLatestReady == VK_TRUE; + if (fifoLatestReady) deviceExtensions.push_back(fifoLatestReadyExtension); + } // Guest dispatches cover whole thread groups past the edge of small images; robust image access // drops those writes instead of faulting the device. const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); @@ -1436,6 +1448,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : minLodFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &minLodFeatures; } + fifoLatestReadyFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_MODE_FIFO_LATEST_READY_FEATURES_KHR}; + fifoLatestReadyFeatures.presentModeFifoLatestReady = VK_TRUE; + if (fifoLatestReady) { + fifoLatestReadyFeatures.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &fifoLatestReadyFeatures; + } if (state->maintenance8) { maintenance8Features.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &maintenance8Features; @@ -1612,6 +1630,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : std::vector formats(formatCount); check(getFormats(selected, state->surface, &formatCount, formats.data()), "vkGetPhysicalDeviceSurfaceFormatsKHR"); require(std::any_of(formats.begin(), formats.end(), [](const auto& format) { return format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; }), "BGRA8 sRGB-nonlinear surface format is unavailable"); + std::uint32_t presentModeCount = 0; + auto getPresentModes = state->InstanceFunction("vkGetPhysicalDeviceSurfacePresentModesKHR"); + check(getPresentModes(selected, state->surface, &presentModeCount, nullptr), "vkGetPhysicalDeviceSurfacePresentModesKHR"); + std::vector presentModes(presentModeCount); + check(getPresentModes(selected, state->surface, &presentModeCount, presentModes.data()), "vkGetPhysicalDeviceSurfacePresentModesKHR"); + presentModes.resize(presentModeCount); + state->presentMode = SelectPresentMode(std::getenv("ANYPS5_PRESENT_MODE"), presentModes); VkSwapchainCreateInfoKHR swapchain{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; swapchain.surface = state->surface; swapchain.minImageCount = surface.minImageCount; @@ -1623,7 +1648,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : swapchain.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchain.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; swapchain.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - swapchain.presentMode = VK_PRESENT_MODE_FIFO_KHR; + swapchain.presentMode = state->presentMode; swapchain.clipped = VK_FALSE; check(state->DeviceFunction("vkCreateSwapchainKHR")(state->device, &swapchain, nullptr, &state->swapchain), "vkCreateSwapchainKHR"); std::uint32_t imageCount = 0; @@ -2302,7 +2327,7 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { create.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; create.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; create.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - create.presentMode = VK_PRESENT_MODE_FIFO_KHR; + create.presentMode = state->presentMode; create.oldSwapchain = state->swapchain; state->retiredSwapchains.reserve(state->retiredSwapchains.size() + 1); VkSwapchainKHR replacement = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index 4ce4faa60..f675373d9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" @@ -298,8 +299,32 @@ void testReset(bool compute) { } +void testPresentMode() { + using AgcDriver::SelectPresentMode; + const std::array wayland{VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR}; + const std::array all{VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR}; + check(SelectPresentMode(nullptr, wayland) == VK_PRESENT_MODE_FIFO_KHR, "the default present mode is not FIFO"); + check(SelectPresentMode("fifo", wayland) == VK_PRESENT_MODE_FIFO_KHR, "fifo was not selected"); + check(SelectPresentMode("immediate", wayland) == VK_PRESENT_MODE_IMMEDIATE_KHR, "immediate was not selected"); + check(SelectPresentMode("mailbox", all) == VK_PRESENT_MODE_MAILBOX_KHR, "mailbox was not selected"); + const std::array latest{VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_FIFO_LATEST_READY_KHR}; + check(SelectPresentMode("fifo_latest_ready", latest) == VK_PRESENT_MODE_FIFO_LATEST_READY_KHR, "fifo_latest_ready was not selected"); + check(SelectPresentMode(nullptr, latest) == VK_PRESENT_MODE_FIFO_KHR, "the default changed on a surface with fifo_latest_ready"); + check(expectFailure([&] { SelectPresentMode("fifo_latest_ready", wayland); }).find("does not support") != std::string::npos, "fifo_latest_ready was accepted on a surface without it"); + check(expectFailure([&] { SelectPresentMode("mailbox", wayland); }).find("does not support") != std::string::npos, "an unsupported present mode was accepted"); + check(expectFailure([&] { SelectPresentMode("Immediate", all); }).find("must be fifo, fifo_latest_ready, mailbox or immediate") != std::string::npos, "an unknown present mode name was accepted"); + check(expectFailure([&] { SelectPresentMode("", all); }).find("must be fifo, fifo_latest_ready, mailbox or immediate") != std::string::npos, "an empty present mode name was accepted"); + const std::array none{}; + check(expectFailure([&] { SelectPresentMode(nullptr, none); }).find("does not support") != std::string::npos, "FIFO was selected on a surface without it"); +} + int main(int argc, char** argv) { try { + if (argc == 2 && std::string(argv[1]) == "present-mode") { + testPresentMode(); + std::puts("present mode selection tests passed"); + return 0; + } if (argc == 2) testReset(std::string(argv[1]) == "compute"); else { testFlipAndBoundary(); testFlipHold(); testFailure(); } const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 12accb979..0b5c3c334 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -131,6 +131,8 @@ Games ask for the console's language with `sceSystemServiceParamGetInt` and show The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. +The window presents with vertical sync (`fifo`). Set `ANYPS5_PRESENT_MODE` to `fifo_latest_ready`, `mailbox` or `immediate` to keep the game from waiting for the display: `fifo_latest_ready` and `mailbox` still show whole frames at the display's refresh and drop the ones in between, `immediate` presents at once and can tear. The start fails on any other value and when the surface does not offer the mode (`fifo_latest_ready` needs `VK_KHR_present_mode_fifo_latest_ready` or its EXT form). Flips stay paced at 59.94 Hz in every mode. A Wayland compositor can stop delivering frames to a window it does not show, for example one on another workspace: on Hyprland with the NVIDIA driver a `fifo` swapchain then presents about once per second, while `fifo_latest_ready` and `immediate` keep the game at its normal rate. + On Windows, the OpenCL, OpenGL, and Vulkan Compatibility Pack can install Mesa Dozen, which exposes D3D12 devices through Vulkan. Some D3D12 drivers can crash while the Vulkan loader enumerates adapters, even when AnyPS5 would select a native Vulkan device. If this happens and the system has another Vulkan driver, disable Dozen for the current PowerShell session before launching AnyPS5: ```powershell From d8b42473d7cf1b223fa96026946db0d9831465af Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:07:04 +0530 Subject: [PATCH 261/318] feat(libkernel): support MIB 1.37 (kern.arandom) in sysctl --- .../System/src/SystemConfiguration.cpp | 19 ++++++++++ core/libs/tests/GuestSystemConfiguration.cpp | 36 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 56 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index a9c9b7ce1..c5c0f2220 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -128,6 +128,21 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void static constexpr char path[] = "/app0/eboot.bin"; return CopyOut(path, sizeof(path), oldValue, oldLength); } + if (nameLength == 2 && name[0] == 1 && name[1] == 37) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + if (oldLength == nullptr) return 0; + const std::size_t count = std::min(*oldLength, static_cast(256)); + if (oldValue != nullptr) { + static std::random_device device; + auto* bytes = static_cast(oldValue); + for (std::size_t i = 0; i < count; ++i) bytes[i] = static_cast(device()); + } + *oldLength = count; + return 0; + } std::string mib; for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); @@ -146,6 +161,10 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 22; return -1; } + if (std::strcmp(name, "kern.arandom") == 0) { + static constexpr int mib[] = {1, 37}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } if (std::strcmp(name, "kern.rng_pseudo") == 0) { if (newValue != nullptr) { *__error_nid_postfix() = 1; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 6ba24d971..069ef2767 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -194,6 +194,41 @@ static void CheckSdkVersionSysctl() { length = sizeof(version); Require(sysctlbyname_nid_postfix("kern.sdk_version", &version, &length, &version, sizeof(version)) == -1 && *__error_nid_postfix() == 1); } +static void CheckArandomSysctl() { + const int mib[] = {1, 37}; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 0); + length = 64; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 64); + length = 300; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 256); + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == 0); + Require(*__error_nid_postfix() == 13); + unsigned char first[64] = {}; + unsigned char second[64] = {}; + length = sizeof(first); + Require(sysctl_nid_postfix(mib, 2, first, &length, nullptr, 0) == 0 && length == sizeof(first)); + length = sizeof(second); + Require(sysctl_nid_postfix(mib, 2, second, &length, nullptr, 0) == 0 && length == sizeof(second)); + Require(std::memcmp(first, second, sizeof(first)) != 0); + Require(*__error_nid_postfix() == 13); + unsigned char named[64] = {}; + length = sizeof(named); + Require(sysctlbyname_nid_postfix("kern.arandom", named, &length, nullptr, 0) == 0 && length == sizeof(named)); + Require(*__error_nid_postfix() == 13); + length = sizeof(first); + Require(sysctl_nid_postfix(mib, 2, first, &length, first, sizeof(first)) == -1 && *__error_nid_postfix() == 1); + *__error_nid_postfix() = 13; + unsigned char big[300] = {}; + length = sizeof(big); + Require(sysctl_nid_postfix(mib, 2, big, &length, nullptr, 0) == 0 && length == 256); + unsigned char small[16] = {}; + length = sizeof(small); + Require(sysctl_nid_postfix(mib, 2, small, &length, nullptr, 0) == 0 && length == sizeof(small)); + Require(sysctl_nid_postfix(mib, 2, first, nullptr, nullptr, 0) == 0); + Require(*__error_nid_postfix() == 13); +} int main() { CheckProcessorCountSysctl(); CheckPhysicalMemorySysctl(); @@ -202,6 +237,7 @@ int main() { CheckSystemSwVersion(); CheckPseudoRandomSysctl(); CheckSdkVersionSysctl(); + CheckArandomSysctl(); *__error_nid_postfix() = 13; Require(sysconf_nid_postfix(47) == 0x4000); Require(getpagesize_nid_postfix() == sysconf_nid_postfix(47)); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 672d11da6..51d7565f0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -670,3 +670,4 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws - [pthread_cond_reltimedwait_np](../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console +- [sysctl kern.arandom](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. From 2781077acec661b576b4479d33666ac3d623c7db Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:04:30 +0530 Subject: [PATCH 262/318] feat(libkernel): answer getrlimit for FreeBSD resources --- .../libs/prx/libkernel/System/src/Process.cpp | 47 ++++++++++++++----- core/libs/tests/GuestGetrlimit.cpp | 40 +++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 32019c4bd..c2d13cd83 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -114,7 +114,16 @@ void validateSchedulingPolicy(int policy) { constexpr int guestFault = 14; constexpr int guestInvalid = 22; +constexpr int guestRlimitCpu = 0; +constexpr int guestRlimitFsize = 1; constexpr int guestRlimitData = 2; +constexpr int guestRlimitCore = 4; +constexpr int guestRlimitSbsize = 9; +constexpr int guestRlimitVmem = 10; +constexpr int guestRlimitNpts = 11; +constexpr int guestRlimitSwap = 12; +constexpr int guestRlimitKqueues = 13; +constexpr int guestRlimitUmtxp = 14; constexpr int guestRlimitCount = 15; constexpr std::int64_t guestRlimitInfinity = std::numeric_limits::max(); @@ -451,20 +460,36 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { *__error_nid_postfix() = guestFault; return -1; } - if (resource != guestRlimitData) - throw std::runtime_error("getrlimit: unsupported resource " + std::to_string(resource)); + switch (resource) { + case guestRlimitData: { #ifdef _WIN32 - const std::int64_t memory = hostMemoryLimit(); - limit->rlim_cur = memory; - limit->rlim_max = memory; + const std::int64_t memory = hostMemoryLimit(); + limit->rlim_cur = memory; + limit->rlim_max = memory; #else - rlimit native{}; - if (::getrlimit(RLIMIT_DATA, &native) != 0) - throw std::system_error(errno, std::generic_category(), "getrlimit: getrlimit failed"); - limit->rlim_cur = toGuestLimit(native.rlim_cur); - limit->rlim_max = toGuestLimit(native.rlim_max); + rlimit native{}; + if (::getrlimit(RLIMIT_DATA, &native) != 0) + throw std::system_error(errno, std::generic_category(), "getrlimit: getrlimit failed"); + limit->rlim_cur = toGuestLimit(native.rlim_cur); + limit->rlim_max = toGuestLimit(native.rlim_max); #endif - return 0; + return 0; + } + case guestRlimitCpu: + case guestRlimitFsize: + case guestRlimitCore: + case guestRlimitSbsize: + case guestRlimitVmem: + case guestRlimitNpts: + case guestRlimitSwap: + case guestRlimitKqueues: + case guestRlimitUmtxp: + limit->rlim_cur = guestRlimitInfinity; + limit->rlim_max = guestRlimitInfinity; + return 0; + default: + throw std::runtime_error("getrlimit: unsupported resource " + std::to_string(resource)); + } } int APS5_VABI sceKernelIsTrinityMode(void) { diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index 598ae8ed9..73b23df87 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -17,9 +17,21 @@ extern "C" int* APS5_VABI __error_nid_postfix(); static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_RLIMIT_CPU = 0; +static constexpr int GUEST_RLIMIT_FSIZE = 1; static constexpr int GUEST_RLIMIT_DATA = 2; +static constexpr int GUEST_RLIMIT_STACK = 3; +static constexpr int GUEST_RLIMIT_CORE = 4; +static constexpr int GUEST_RLIMIT_RSS = 5; +static constexpr int GUEST_RLIMIT_MEMLOCK = 6; +static constexpr int GUEST_RLIMIT_NPROC = 7; static constexpr int GUEST_RLIMIT_NOFILE = 8; +static constexpr int GUEST_RLIMIT_SBSIZE = 9; static constexpr int GUEST_RLIMIT_AS = 10; +static constexpr int GUEST_RLIMIT_NPTS = 11; +static constexpr int GUEST_RLIMIT_SWAP = 12; +static constexpr int GUEST_RLIMIT_KQUEUES = 13; +static constexpr int GUEST_RLIMIT_UMTXP = 14; static constexpr int GUEST_RLIM_NLIMITS = 15; static constexpr std::int64_t GUEST_RLIM_INFINITY = std::numeric_limits::max(); @@ -80,11 +92,35 @@ int main() { RequireHostLimit(GUEST_RLIMIT_DATA, RLIMIT_DATA); LoweredHostLimitIsReported(); #endif + for (int resource : { + GUEST_RLIMIT_CPU, + GUEST_RLIMIT_FSIZE, + GUEST_RLIMIT_CORE, + GUEST_RLIMIT_SBSIZE, + GUEST_RLIMIT_AS, + GUEST_RLIMIT_NPTS, + GUEST_RLIMIT_SWAP, + GUEST_RLIMIT_KQUEUES, + GUEST_RLIMIT_UMTXP, + }) { + const ResourceLimit guest = GuestLimit(resource); + Require(guest.rlim_cur == GUEST_RLIM_INFINITY); + Require(guest.rlim_max == GUEST_RLIM_INFINITY); + RequireError(resource, nullptr, GUEST_EFAULT); + } ResourceLimit limit{}; RequireError(GUEST_RLIM_NLIMITS, &limit, GUEST_EINVAL); RequireError(-1, &limit, GUEST_EINVAL); RequireError(GUEST_RLIM_NLIMITS, nullptr, GUEST_EINVAL); + RequireError(-1, nullptr, GUEST_EINVAL); RequireError(GUEST_RLIMIT_DATA, nullptr, GUEST_EFAULT); - RequireUnsupported(GUEST_RLIMIT_NOFILE); - RequireUnsupported(GUEST_RLIMIT_AS); + for (int resource : { + GUEST_RLIMIT_STACK, + GUEST_RLIMIT_RSS, + GUEST_RLIMIT_MEMLOCK, + GUEST_RLIMIT_NPROC, + GUEST_RLIMIT_NOFILE, + }) { + RequireUnsupported(resource); + } } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 672d11da6..15c7b4705 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -461,6 +461,7 @@ Throughout the project, every function at every stage either **does exactly what - [execv](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [kern.proc.pathname](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [machdep.cacheline_size](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. +- [getrlimit](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit. `RLIMIT_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK`, `RLIMIT_NPROC` and `RLIMIT_NOFILE` throw because no console values are established. - [execvp](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [MIMG BY2/BY4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. From 22d662eda76145501e9afa065932fac4f93dbfa8 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 19:39:43 +1300 Subject: [PATCH 263/318] fix(agc): export the stencil reference from pixel shaders with VK_EXT_shader_stencil_export --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Execution/src/VulkanDevice.cpp | 5 + .../Graphics/src/ShaderInputState.cpp | 6 +- .../Graphics/src/ShaderValidation.cpp | 13 +- .../libSceAgcDriver/Graphics/src/State.cpp | 17 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 126 ++++++++++++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 16 +- .../tests/execution/StencilRefExport.cpp | 159 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../IrMetadata/StageIO.hpp | 1 + .../Optimization/ShaderStageInputInfo.hpp | 1 + .../Optimization/src/ShaderInfoCollector.cpp | 3 + core/shader/recompiler/Recompiler.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 3 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 10 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 6 + .../src/ShaderInputInfoBuilder.cpp | 1 + docs/dev/TechnicalDebt.md | 1 + tools/tests/test_check_skips.py | 1 + 22 files changed, 363 insertions(+), 18 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 14090d411..7b959293e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -64,6 +64,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); mix(pixel.orderedPixelShader); + mix(pixel.stencilRefExportEnable); for (const auto value : pixel.targetOutputMode) mix(value); for (const auto value : pixel.targetExportMapping) mix(value); for (const auto value : pixel.targetExportPacking) mix(static_cast(value)); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 3e1fd6a77..6dec41300 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -98,7 +98,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false; const auto& p = a.pixel; const auto& q = b.pixel; - if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; + if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false; for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index d24dbbc56..0f03fb022 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1095,6 +1095,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT); state->spirvExtensions.push_back("SPV_EXT_fragment_shader_interlock"); } + if (hasExtension(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME)) { + deviceExtensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityStencilExportEXT); + state->spirvExtensions.push_back("SPV_EXT_shader_stencil_export"); + } if (state->shaderClock) { deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityShaderClockKHR); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 3e89ca766..61cfa6b42 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -142,6 +142,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0; const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; + const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; const auto zOrder = (shaderControl >> 4u) & 0x3u; const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; return ShaderRecompiler::ShaderPixelStageInfo{ @@ -163,12 +164,13 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .pixelKillEnable = pixelKillEnable, .depthExportEnable = depthExportEnable, .sampleMaskExportEnable = sampleMaskExportEnable, - .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, + .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable, .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, .conservativeZExport = static_cast(conservativeZExport), .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, .targetOutputMode = targetOutputMode, - .targetExportMapping = nullProgram ? std::array{} : exportMappings + .targetExportMapping = nullProgram ? std::array{} : exportMappings, + .stencilRefExportEnable = stencilRefExportEnable }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index d30076237..029c89209 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -137,7 +137,7 @@ struct Module { Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output"); position = true; } else { - Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); + Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); } } }; @@ -232,6 +232,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi fragment && capability == spv::CapabilityFragmentShaderPixelInterlockEXT; + const bool isStencilExportCapability = + fragment && + capability == spv::CapabilityStencilExportEXT; + // Enabled unconditionally or by the device setup in VulkanDevice. const bool isFeatureCapability = capability == spv::CapabilitySampled1D || @@ -273,6 +277,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi isMeshCapability || isGeometryCapability || isInterlockCapability || + isStencilExportCapability || isFeatureCapability || isDescriptorIndexingCapability || ((imageInt64Atomics || bufferInt64Atomics) && capability == spv::CapabilityInt64Atomics) || @@ -293,6 +298,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment, "SPV_EXT_fragment_shader_interlock requires a fragment shader"); break; } + if (extension == "SPV_EXT_shader_stencil_export") { + Require(fragment, "SPV_EXT_shader_stencil_export requires a fragment shader"); + break; + } if (extension == "SPV_KHR_fragment_shader_barycentric") { Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; @@ -420,7 +429,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeOutputVertices, {3u}); Require(module.modes.size() == 4, "unsupported geometry execution mode"); } else if (fragment) { - for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT), "unsupported fragment execution mode"); + for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT || name == spv::ExecutionModeStencilRefReplacingEXT), "unsupported fragment execution mode"); } else Require(module.modes.empty(), "unsupported vertex execution mode"); std::set> descriptors; bool push = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index ce50da558..2be0ff53a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -119,9 +119,11 @@ bool zFormatSupported(std::uint32_t format) { std::uint32_t shaderControlMask(std::uint32_t zFormat) { constexpr std::uint32_t zExportEnable = 0x1u; + constexpr std::uint32_t stencilTestValExportEnable = 0x2u; constexpr std::uint32_t maskExportEnable = 0x100u; auto mask = ShaderControlMask; if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable; + if (zFormat == 2u || zFormat == 9u) mask &= ~stencilTestValExportEnable; if (zFormat == 9u) mask &= ~maskExportEnable; return mask; } @@ -154,7 +156,7 @@ bool depthPlanesAbsent(const Registers& cx) { return z != cx.end() && stencil != cx.end() && (z->second & 3u) == 0 && (stencil->second & 1u) == 0; } -VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly) { +VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly, bool testValExport) { VkStencilOpState face{}; face.compareOp = static_cast(compare); face.compareMask = (refMask >> 8u) & 0xffu; @@ -172,6 +174,10 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint case 2: case 3: case 4: { + if (testValExport) { + if (op != 3) throw std::runtime_error("AGC graphics: stencil replacement by a value other than the test value is unsupported with a pixel shader that exports the stencil test value"); + return VK_STENCIL_OP_REPLACE; + } const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue; if (((reference ^ value) & fixed & face.writeMask) != 0) { std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask); @@ -203,7 +209,7 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint return face; } -void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) { +void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, State& result) { zero(cx, 0x000, 0x00001f9cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const bool depthClear = (read(cx, 0x000) & 1u) != 0; const bool stencilClear = (read(cx, 0x000) & 2u) != 0; @@ -223,6 +229,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) const bool depthReadOnly = (view & 0x01000000u) != 0; const bool stencilReadOnly = (view & 0x02000000u) != 0; Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane"); + Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported"); Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane"); DepthTarget depth{}; depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); @@ -274,8 +281,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) result.stencilBack = clear; } else if (result.stencilTest) { const auto ops = read(cx, 0x10b); - result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly); - result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : result.stencilFront; + result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly, testValExport); + result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly, testValExport) : result.stencilFront; } } @@ -667,7 +674,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0; const auto tests = planeTests(cx, depthControl); if (clear || (tests != 0 && depthSurfaceBound(cx))) { - decodeDepth(cx, depthControl, result); + decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), result); } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) { static bool reported = false; if (!reported) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 39c8dd8b2..ae0f814f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3688,6 +3688,131 @@ void orderedPixelShaderTests() { expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); } +constexpr std::array StencilRefExportCode{0x7e0e02f2u, 0xf8001882u, 0x700u, 0xbf810000u}; +constexpr std::array DepthStencilRefExportCode{0x7e0e02f2u, 0xf8001883u, 0x707u, 0xbf810000u}; + +AgcDriver::QueueState stencilRefExportQueue(std::uint32_t shaderControl, std::uint32_t zFormat) { + auto queue = makeState(); + queue.context[0x1b3] = 0x300u; + queue.context[0x1b4] = 0x300u; + queue.context[0x1c4] = zFormat; + queue.context[0x203] = shaderControl; + return queue; +} + +ShaderRecompiler::RecompileRequest stencilRefExportRequest(std::uint32_t shaderControl, std::span capabilities, std::span code = StencilRefExportCode) { + const auto queue = stencilRefExportQueue(shaderControl, 2u); + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +std::string stencilRefShape(std::span words) { + std::string shape; + std::uint32_t variable = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpCapability && words[at + 1] == spv::CapabilityStencilExportEXT) shape += "capability "; + if (op == spv::OpExtension && std::string_view(reinterpret_cast(&words[at + 1])) == "SPV_EXT_shader_stencil_export") shape += "extension "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) shape += "early "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeDepthReplacing) shape += "depth "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeStencilRefReplacingEXT) shape += "replacing "; + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInFragStencilRefEXT) { + variable = words[at + 1]; + shape += "builtin "; + } + if (op == spv::OpStore && variable != 0 && words[at + 1] == variable) shape += "store "; + } + return shape; +} + +void stencilRefExportTests() { + for (const auto shaderControl : {0x52u, 0x12u, 0x02u, 0x03u}) { + for (const auto zFormat : {2u, 9u}) { + const auto queue = stencilRefExportQueue(shaderControl, zFormat); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with Z format " + std::to_string(zFormat)); + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + Require(pixel.stencilRefExportEnable, "STENCIL_TEST_VAL_EXPORT_ENABLE did not reach the pixel stage"); + Require(!pixel.earlyZ, "a pixel shader that exports the stencil reference was given early tests"); + } + for (const auto zFormat : {0u, 1u, 3u, 7u}) { + const auto queue = stencilRefExportQueue(shaderControl, zFormat); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); + Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted a stencil reference export with Z format " + std::to_string(zFormat)); + } + } + for (const auto shaderControl : {0x54u, 0x56u}) { + const auto queue = stencilRefExportQueue(shaderControl, 2u); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); + Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl)); + } + Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference"); + const std::array stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT}; + const std::array plain{spv::CapabilityShader}; + const ShaderRecompiler::RequestSerializer serializer; + std::set> keys; + const std::array, 4> cases{{{0x52u, "capability extension replacing builtin store "}, {0x12u, "capability extension replacing builtin store "}, {0x50u, ""}, {0x10u, "early "}}}; + for (const auto& [shaderControl, expected] : cases) { + const auto request = stencilRefExportRequest(shaderControl, stencilExport); + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(replayed.request.context.pixel->stencilRefExportEnable == ((shaderControl & 2u) != 0), "the serialized request lost STENCIL_TEST_VAL_EXPORT_ENABLE"); + const auto result = ShaderRecompiler::Recompile(request); + Require(stencilRefShape(result.spirv.Words()) == expected && stencilRefShape(ShaderRecompiler::Recompile(replayed.request).spirv.Words()) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong stencil reference export: " + stencilRefShape(result.spirv.Words())); + std::vector key; + ShaderRecompiler::RecompileCacheKey::Build(request, key); + keys.insert(key); + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &result, 0}}}; + AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); + } + Require(keys.size() == cases.size(), "the recompile cache key ignores STENCIL_TEST_VAL_EXPORT_ENABLE"); + const auto both = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x53u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); + Require(both == "capability extension depth replacing builtin store ", "a depth and stencil reference export declared the wrong outputs: " + both); + const auto depthOnly = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x51u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); + Require(depthOnly == "depth ", "an MRTZ stencil channel without STENCIL_TEST_VAL_EXPORT_ENABLE declared the wrong outputs: " + depthOnly); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain))); }, "VK_EXT_shader_stencil_export"); + auto queue = stencilRefExportQueue(0x52u, 2u); + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (1u << 16u) | 3u; + queue.context[0x00a] = 7; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x22900983; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x00030030; + queue.context[0x10c] = 0x05ffff02; + queue.context[0x10d] = 0x05ffff02; + queue.context[0x200] = 0x00200211; + auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilTest && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.passOp == VK_STENCIL_OP_REPLACE, "a stencil reference export changed the REPLACE_TEST decode"); + for (const auto ops : {0x00020020u, 0x00040040u, 0x00000200u, 0x00000004u}) { + queue.context[0x10b] = ops; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "exports the stencil test value"); + } + queue.context[0x10b] = 0x00050050; + queue.context[0x10c] = 0x01ffff02; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP, "a stencil reference export changed the increment decode"); + queue.context[0x000] = 2; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "stencil clear with a pixel shader that exports the stencil test value"); + queue.context[0x203] = 0x50u; + static_cast(AgcDriver::Graphics::DecodeState(queue)); +} + void ProvokingVertexTests() { using namespace AgcDriver::Graphics; constexpr std::uint32_t lastVertex = 1u << 19u; @@ -4705,6 +4830,7 @@ int main(int argc, char** argv) { DepthBoundsBiasTests(); conservativeZExportTests(); orderedPixelShaderTests(); + stencilRefExportTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1526845de..bf1f92b8c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1542,7 +1542,7 @@ void verifyPixelRequestSerialization() { value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -1586,6 +1586,7 @@ void verifyPixelRequestSerialization() { }; for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; pixel.alphaToCoverage = true; + pixel.stencilRefExportEnable = true; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -1614,6 +1615,10 @@ void verifyPixelRequestSerialization() { withoutCoverage.context.pixel->alphaToCoverage = false; RecompileCacheKey::Build(withoutCoverage, replayKey); require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutCoverage), "alpha-to-coverage did not change the static shader identity"); + auto withoutStencilRef = request; + withoutStencilRef.context.pixel->stencilRefExportEnable = false; + RecompileCacheKey::Build(withoutStencilRef, replayKey); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutStencilRef), "the stencil reference export did not change the static shader identity"); } request.context.pixel.reset(); request.shader.stage = ShaderStage::Vertex; @@ -1625,12 +1630,12 @@ void verifyPixelRequestSerialization() { minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRAAAAA=", "new requests did not use serialization version 16"); + require(requestPrefix(encoded, 8u) == "NVNQQREAAAA=", "new requests did not use serialization version 17"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 18u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source or alpha-to-coverage flag was accepted"); + for (std::size_t bytes = 0; bytes < 19u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage or stencil reference export flag was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQREAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRIAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } } @@ -1720,6 +1725,7 @@ void verifyLegacyPixelRequests() { require(pixel.targetExportPacking == std::array{}, "a legacy request packed a color export"); require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending"); require(!pixel.alphaToCoverage, "a legacy request enabled alpha-to-coverage"); + require(!pixel.stencilRefExportEnable, "a legacy request exported a stencil reference"); require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp new file mode 100644 index 000000000..ac0318165 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp @@ -0,0 +1,159 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PlainPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array ColumnPixelCode{ + 0x7e040f00, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array RowPixelCode{ + 0x7e040f01, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array, 3> Vertices{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { + const AgcDriver::Registers context{ + {0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, shaderControl}, + }; + return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil) { + Pixels.fill(Kept); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(shaderControl), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), reinterpret_cast(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}; + state.stencilTest = true; + state.stencilFront = stencil; + state.stencilBack = stencil; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const std::function& drawn, std::string_view what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = drawn(x, y) ? std::byte{255} : Kept; + const auto actual = Pixels[(y * Width + x) * 4u]; + Require(actual == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "stencil-rejected")); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityStencilExportEXT)) { + std::puts("skipped, the device lacks VK_EXT_shader_stencil_export"); + return VulkanTestSkipped; + } + constexpr std::uint32_t exported = 0x52u; + constexpr std::uint32_t plain = 0x50u; + const VkStencilOpState replace{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u}; + const auto equal = [](std::uint32_t reference) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, reference}; }; + Draw(*device, ColumnPixelCode, exported, replace); + Check([](std::uint32_t, std::uint32_t) { return true; }, "the draw that replaces the stencil with the exported column"); + for (const auto column : {0u, 5u, 37u, 63u}) { + Draw(*device, PlainPixelCode, plain, equal(column)); + Check([&](std::uint32_t x, std::uint32_t) { return x == column; }, "stencil equal to " + std::to_string(column) + " after the exported column was stored"); + } + Draw(*device, ColumnPixelCode, exported, equal(200u)); + Check([](std::uint32_t, std::uint32_t) { return true; }, "the exported column tested against the stored column"); + Draw(*device, RowPixelCode, exported, equal(200u)); + Check([](std::uint32_t x, std::uint32_t y) { return x == y; }, "the exported row tested against the stored column"); + Draw(*device, ColumnPixelCode, plain, equal(9u)); + Check([](std::uint32_t x, std::uint32_t) { return x == 9u; }, "a column export without STENCIL_TEST_VAL_EXPORT_ENABLE"); + std::puts("stencil reference export tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index be68766aa..27816a7d9 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -177,6 +177,7 @@ private: append(key, value.conservativeZExport); append(key, value.orderedPixelShader); append(key, value.alphaToCoverage); + append(key, value.stencilRefExportEnable); append(key, value.targetOutputMode); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 5a76e67bb..fe32878c4 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -325,6 +325,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { } writer.WriteBool(info.dualSourceBlend); writer.WriteBool(info.alphaToCoverage); + writer.WriteBool(info.stencilRefExportEnable); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -373,6 +374,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { } if (version >= 15u) info.dualSourceBlend = reader.ReadBool(); if (version >= 16u) info.alphaToCoverage = reader.ReadBool(); + if (version >= 17u) info.stencilRefExportEnable = reader.ReadBool(); if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -734,7 +736,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(16u); + writer.WriteU32(17u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -762,7 +764,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 16u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 17u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 4046804ef..2c5b7c379 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -51,6 +51,7 @@ enum class StageOutputKind { Mrt, Depth, SampleMask, + StencilRef, PointSize, ClipDistance, CullDistance, diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 32300b030..ba65e3242 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -167,6 +167,7 @@ struct ShaderPixelInputInfo { bool psPixelKillEnable = false; bool psDepthExportEnable = false; bool psSampleMaskExportEnable = false; + bool psStencilRefExportEnable = false; bool psSampleShading = false; bool psEarlyZ = false; bool psExecuteOnNoop = false; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 00bde0c18..7ea25b14f 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -408,6 +408,9 @@ void CollectOutputs(const IrProgram& program, ShaderStageInputInfo inputInfo, Sh if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x4u) != 0u && inputInfo.pixel->psSampleMaskExportEnable) { AddOutput(info, StageOutputKind::SampleMask, 0, 0, "gl_SampleMask"); } + if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x2u) != 0u && inputInfo.pixel->psStencilRefExportEnable) { + AddOutput(info, StageOutputKind::StencilRef, 0, 0, "gl_FragStencilRefARB"); + } continue; } if (exportInfo.en == 0u) { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 85f9689e8..8714fc3d1 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -141,6 +141,7 @@ struct ShaderPixelStageInfo { std::array targetExportPacking; bool dualSourceBlend; bool alphaToCoverage; + bool stencilRefExportEnable; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index ca69aa93c..1a63e7baa 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -171,6 +171,7 @@ struct SpirvEmitterState { std::uint32_t viewportIndexVariable = 0; std::uint32_t depthVariable = 0; std::uint32_t sampleMaskVariable = 0; + std::uint32_t stencilRefVariable = 0; std::vector inputs; std::vector outputs; std::vector interfaceVariables; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index c484fa2b8..f0d347e5b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -359,6 +359,9 @@ void DefineOutputs(SpirvEmitterState& state) { case StageOutputKind::SampleMask: binding.variableId = BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); break; + case StageOutputKind::StencilRef: + binding.variableId = BuiltIn(state.stencilRefVariable, TypeI32(state), "gl_FragStencilRefARB", spv::BuiltInFragStencilRefEXT); + break; case StageOutputKind::Parameter: case StageOutputKind::Mrt: { const bool uintOutput = binding.kind == StageOutputKind::Mrt && state.program.Resources().stage == IrShaderStage::Pixel && binding.index < std::size(PixelInfo(state).targetOutputMode) && PixelInfo(state).targetOutputMode[binding.index] == 7u; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 699290232..a6ada9eba 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -185,7 +185,15 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater); } } - if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !pixel.psAlphaToCoverage) { + if (state.stencilRefVariable != 0) { + if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) { + throw std::runtime_error("a pixel shader that exports the stencil reference needs VK_EXT_shader_stencil_export, which the device lacks"); + } + state.module.EmitCapability(spv::CapabilityStencilExportEXT); + state.module.EmitExtension("SPV_EXT_shader_stencil_export"); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); + } + if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !pixel.psStencilRefExportEnable && !pixel.psAlphaToCoverage) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1793ea2f8..51f94b566 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1298,6 +1298,12 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { state.module.AddFunction(spv::OpBitcast, TypeF32(state), f32, raw); state.module.AddFunction(spv::OpStore, state.depthVariable, f32); } + if ((exp.en & 2u) != 0u && state.stencilRefVariable != 0u) { + const auto raw = ExportRawComponent(ctx, data, 1); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw); + state.module.AddFunction(spv::OpStore, state.stencilRefVariable, value); + } if ((exp.en & 4u) != 0u && state.sampleMaskVariable != 0u) { const auto raw = ExportRawComponent(ctx, data, 2); const auto value = state.module.AllocateId(); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 79791905b..ce067c491 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -136,6 +136,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psPixelKillEnable = pixel.pixelKillEnable; pixelStorage.psDepthExportEnable = pixel.depthExportEnable; pixelStorage.psSampleMaskExportEnable = pixel.sampleMaskExportEnable; + pixelStorage.psStencilRefExportEnable = pixel.stencilRefExportEnable; pixelStorage.psSampleShading = pixel.sampleShading; pixelStorage.psEarlyZ = pixel.earlyZ; pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f42d54e58..80dd923f7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -541,6 +541,7 @@ Throughout the project, every function at every stage either **does exactly what - The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. +- [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT`, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw, the UINT16_ABGR layout, `STENCIL_OP_VAL_EXPORT_ENABLE` and a device without `VK_EXT_shader_stencil_export` (NVIDIA's driver 610.57 offers none). - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 02ab77f59..d8ba15c75 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -13,6 +13,7 @@ LIMITATIONS = [ "skipped, the device lacks fragmentShaderPixelInterlock", "skipped, the device lacks geometryShader", "skipped, the device lacks shaderSubgroupClock", + "skipped, the device lacks VK_EXT_shader_stencil_export", "skipped, the device does not import guest memory", "skipped, the device imports no host memory", "skipped, the device flushes f64 subnormals without DenormPreserve", From 591e8ddadc25bfe7f54d563e8890d351fc601bb3 Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 18:00:28 +0200 Subject: [PATCH 264/318] feat(libkernel): support fcntl on non-socket descriptors fcntl and sceKernelFcntl answered EBADF (sceKernelFcntl threw) for every descriptor outside the libSceNet socket table, so files, pipes, directories and descriptors 0 to 2 could not be queried or changed. Observed: Yamagi Quake II (PPSA99007) stops with "Sys_Error: change stdin to blocking Bad file descriptor", from fcntl(fileno(stdin), F_SETFL, fcntl(0, F_GETFL, 0) & ~FNDELAY) (yquake2 src/backends/unix/system.c:81-86). Predicted from the titles' sources and decoded call sites: XPSemu (PPSA97358, 97 fcntl sites: F_GETFD 12, F_SETFD 20, F_GETFL 8, F_SETFL 9, F_DUPFD 1, F_DUPFD_CLOEXEC 7, F_GETLK 9) fails in qemu_init_main_loop, where its pipe2 (pipe plus F_SETFD FD_CLOEXEC) and glib's g_unix_set_fd_nonblocking (F_GETFL, F_SETFL) build the event notifier (util/event_notifier-posix.c:44-67); VLC (PPSA85300, 43 sites, F_RDAHEAD on every file it opens), RetroArch (PPSA99169, 17), Porpoise (PPSA99764, 13) and Ruffle (9) call it on files and pipes. The commands follow FreeBSD 11.4 kern_fcntl and kern_dup (sys/kern/kern_descrip.c:479-799, 812, release/11.4.0 8c639945): F_GETFD/F_SETFD keep the close-on-exec flag per descriptor, F_GETFL reports the host access mode with the status flags, F_SETFL changes only O_NONBLOCK, O_APPEND, O_ASYNC, O_FSYNC, O_DIRECT and FRDAHEAD, F_DUPFD/F_DUPFD_CLOEXEC give the lowest free descriptor at or above the start through dup (EINVAL for a negative start), F_DUP2FD goes through dup2, F_RDAHEAD/F_READAHEAD set FRDAHEAD on files (EBADF on pipes, ENOTTY elsewhere), and unknown commands fail with EINVAL. Status flags live in a guest table shared by dup, dup2 and F_DUPFD like an open file description. O_NONBLOCK switches a pipe (PIPE_NOWAIT on Windows, where an empty read and a full write now report EAGAIN; the host flag on Linux). Record locks use the host POSIX locks on Linux and throw on Windows. Changes the host cannot model throw naming the flag and descriptor kind. Sockets keep their path unchanged. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libkernel/CMakeLists.txt | 1 + .../File/include/DescriptorControl.hpp | 16 + .../libkernel/File/src/DescriptorControl.cpp | 421 ++++++++++++++++++ core/libs/prx/libkernel/File/src/Open.cpp | 48 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 29 ++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 15 + core/libs/tests/GuestDup.cpp | 30 ++ core/libs/tests/GuestKernelFcntl.cpp | 178 +++++++- docs/dev/TechnicalDebt.md | 3 +- 9 files changed, 708 insertions(+), 33 deletions(-) create mode 100644 core/libs/prx/libkernel/File/include/DescriptorControl.hpp create mode 100644 core/libs/prx/libkernel/File/src/DescriptorControl.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 58c5364b0..ecd85b4b9 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Apr/src/Apr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/Equeue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DescriptorControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DirectoryDescriptor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/FileLock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/NativeStat.cpp diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp new file mode 100644 index 000000000..195fa099d --- /dev/null +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -0,0 +1,16 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP + +#include + +namespace File { + +int Fcntl(int fd, int command, std::intptr_t argument); +int DuplicateFrom(int fd, int start); +void RememberDescriptor(int fd, int openFlags); +void ShareDescriptor(int from, int to); +void ForgetDescriptor(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp new file mode 100644 index 000000000..ee8b4e0d0 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -0,0 +1,421 @@ +#include "prx/libkernel/File/include/DescriptorControl.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); +#ifdef _WIN32 +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#endif + +namespace { + +constexpr int GuestEintr = 4; +constexpr int GuestEbadf = 9; +constexpr int GuestEdeadlk = 11; +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr int GuestEnotty = 25; +constexpr int GuestEagain = 35; +constexpr int GuestEnolck = 77; +constexpr int GuestEoverflow = 84; + +constexpr int DuplicateCommand = 0; +constexpr int GetDescriptorFlags = 1; +constexpr int SetDescriptorFlags = 2; +constexpr int GetStatusFlags = 3; +constexpr int SetStatusFlags = 4; +constexpr int GetOwner = 5; +constexpr int SetOwner = 6; +constexpr int OldGetLock = 7; +constexpr int OldSetLock = 8; +constexpr int OldSetLockWait = 9; +constexpr int DuplicateToCommand = 10; +constexpr int GetLock = 11; +constexpr int SetLock = 12; +constexpr int SetLockWait = 13; +constexpr int SetLockRemote = 14; +constexpr int ReadAhead = 15; +constexpr int DarwinReadAhead = 16; +constexpr int DuplicateCloseOnExec = 17; +constexpr int DuplicateToCloseOnExec = 18; + +constexpr int CloseOnExec = 1; +constexpr int OpenCloseOnExec = 0x00100000; + +constexpr int ReadOnly = 0; +constexpr int WriteOnly = 1; +constexpr int ReadWrite = 2; +constexpr int Nonblocking = 0x0004; +constexpr int Append = 0x0008; +constexpr int Asynchronous = 0x0040; +constexpr int Synchronous = 0x0080; +constexpr int ReadAheadFlag = 0x0200; +constexpr int Direct = 0x00010000; +constexpr int OpenStatusFlags = Nonblocking | Append | Asynchronous | Synchronous | Direct; +constexpr int SettableStatusFlags = OpenStatusFlags | ReadAheadFlag; + +constexpr std::int16_t ReadLock = 1; +constexpr std::int16_t Unlock = 2; +constexpr std::int16_t WriteLock = 3; +constexpr std::int16_t FromCurrent = 1; + +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; +static_assert(sizeof(GuestLock) == 32); +static_assert(offsetof(GuestLock, type) == 20); +constexpr std::size_t OldGuestLockSize = 24; + +enum class Kind { File, Directory, Pipe, Device, RandomDevice }; + +struct Entry { + bool closeOnExec = false; + std::shared_ptr status; +}; + +std::mutex g_mutex; +std::map g_entries; + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +const char* KindName(Kind kind) { + switch (kind) { + case Kind::File: return "file"; + case Kind::Directory: return "directory"; + case Kind::Pipe: return "pipe"; + case Kind::Device: return "character device"; + case Kind::RandomDevice: return "random device"; + } + return "descriptor"; +} + +[[noreturn]] void Unsupported(const char* function, const std::string& what, Kind kind, int fd) { + throw std::runtime_error(std::string(function) + ": " + what + " on a " + KindName(kind) + " is not implemented, fd=" + std::to_string(fd)); +} + +#ifdef _WIN32 +void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} + +HANDLE NativeHandle(int fd) { + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const auto handle = ::_get_osfhandle(fd); + _set_thread_local_invalid_parameter_handler(previous); + return reinterpret_cast(handle); +} + +std::optional Classify(int fd, const char* function) { + if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return std::nullopt; + if (File::DirectoryDescriptorPath(fd)) return Kind::Directory; + if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + switch (::GetFileType(NativeHandle(fd))) { + case FILE_TYPE_DISK: return Kind::File; + case FILE_TYPE_PIPE: return Kind::Pipe; + case FILE_TYPE_CHAR: return Kind::Device; + default: throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " has an unknown Windows file type, error=" + std::to_string(::GetLastError())); + } +} + +int AccessMode(int fd, const char* function) { + using QueryObject = NTSTATUS(NTAPI*)(HANDLE, OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG); + static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"))); + PUBLIC_OBJECT_BASIC_INFORMATION information{}; + const NTSTATUS status = query == nullptr ? -1 : query(NativeHandle(fd), ObjectBasicInformation, &information, sizeof(information), nullptr); + if (status < 0) + throw std::runtime_error(std::string(function) + ": cannot read the access of descriptor " + std::to_string(fd) + ", status=" + std::to_string(status)); + const bool read = (information.GrantedAccess & FILE_READ_DATA) != 0; + const bool write = (information.GrantedAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0; + if (read && write) return ReadWrite; + if (write) return WriteOnly; + if (read) return ReadOnly; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " grants neither read nor write access"); +} + +DWORD PipeState(int fd, const char* function) { + DWORD state = 0; + if (!::GetNamedPipeHandleStateW(NativeHandle(fd), &state, nullptr, nullptr, nullptr, nullptr, 0)) + throw std::runtime_error(std::string(function) + ": cannot read the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); + return state; +} + +void SetPipeNonblocking(int fd, bool nonblocking, const char* function) { + DWORD mode = (PipeState(fd, function) & PIPE_READMODE_MESSAGE) | (nonblocking ? PIPE_NOWAIT : PIPE_WAIT); + if (!::SetNamedPipeHandleState(NativeHandle(fd), &mode, nullptr, nullptr)) + throw std::runtime_error(std::string(function) + ": cannot change the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); +} + +Entry InitialEntry(int fd) { + int status = 0; + if (::GetFileType(NativeHandle(fd)) == FILE_TYPE_PIPE && (PipeState(fd, "fcntl") & PIPE_NOWAIT) != 0) status |= Nonblocking; + return {false, std::make_shared(status)}; +} + +void ApplyHostStatus(int fd, Kind kind, int changed, int next, const char* function) { + if ((changed & Append) && kind == Kind::File) Unsupported(function, "changing O_APPEND", kind, fd); + if ((changed & Nonblocking) && kind == Kind::Device) Unsupported(function, "changing O_NONBLOCK", kind, fd); + if ((changed & Nonblocking) && kind == Kind::Pipe) SetPipeNonblocking(fd, (next & Nonblocking) != 0, function); +} + +#else +std::optional Classify(int fd, const char* function) { + if (fd < 0) return std::nullopt; + struct stat status{}; + if (::fstat(fd, &status) != 0) { + if (errno == EBADF) return std::nullopt; + throw std::runtime_error(std::string(function) + ": fstat failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + } + if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + if (S_ISREG(status.st_mode)) return Kind::File; + if (S_ISDIR(status.st_mode)) return Kind::Directory; + if (S_ISFIFO(status.st_mode)) return Kind::Pipe; + if (S_ISCHR(status.st_mode)) return Kind::Device; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " is neither a file, a directory, a pipe nor a character device"); +} + +int HostStatus(int fd, const char* function) { + const int flags = ::fcntl(fd, F_GETFL); + if (flags < 0) throw std::runtime_error(std::string(function) + ": host F_GETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return flags; +} + +int AccessMode(int fd, const char* function) { + return HostStatus(fd, function) & O_ACCMODE; +} + +Entry InitialEntry(int fd) { + const int descriptorFlags = ::fcntl(fd, F_GETFD); + if (descriptorFlags < 0) throw std::runtime_error("fcntl: host F_GETFD failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + const int host = HostStatus(fd, "fcntl"); + int status = 0; + if (host & O_NONBLOCK) status |= Nonblocking; + if (host & O_APPEND) status |= Append; + if (host & O_ASYNC) status |= Asynchronous; + if ((host & O_SYNC) == O_SYNC) status |= Synchronous; + if (host & O_DIRECT) status |= Direct; + return {(descriptorFlags & FD_CLOEXEC) != 0, std::make_shared(status)}; +} + +void ApplyHostStatus(int fd, Kind, int changed, int next, const char* function) { + if ((changed & (Nonblocking | Append)) == 0) return; + const int host = HostStatus(fd, function); + const int wanted = (host & ~(O_NONBLOCK | O_APPEND)) | ((next & Nonblocking) ? O_NONBLOCK : 0) | ((next & Append) ? O_APPEND : 0); + if (::fcntl(fd, F_SETFL, wanted) != 0) + throw std::runtime_error(std::string(function) + ": host F_SETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); +} + + +int LockError(int error) { + switch (error) { + case EACCES: + case EAGAIN: return GuestEagain; + case EDEADLK: return GuestEdeadlk; + case EINTR: return GuestEintr; + case ENOLCK: return GuestEnolck; + case EBADF: return GuestEbadf; + case EINVAL: return GuestEinval; + case EOVERFLOW: return GuestEoverflow; + default: throw std::runtime_error("fcntl: host record lock failed, errno=" + std::to_string(error)); + } +} +#endif + +Entry& Materialize(int fd) { + const auto found = g_entries.find(fd); + if (found != g_entries.end()) return found->second; + return g_entries.emplace(fd, InitialEntry(fd)).first->second; +} + +int ChangeStatus(int fd, Kind kind, int mask, int value, const char* function) { + std::lock_guard lock(g_mutex); + auto& status = *Materialize(fd).status; + const int next = (status & ~mask) | (value & mask); + const int changed = status ^ next; + if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); + if ((changed & Synchronous) && kind == Kind::File) Unsupported(function, "changing O_FSYNC", kind, fd); + ApplyHostStatus(fd, kind, changed, next, function); + status = next; + return 0; +} + +int CurrentStatus(int fd) { + std::lock_guard lock(g_mutex); + return *Materialize(fd).status; +} + +void SetCloseOnExec(int fd, bool value) { + std::lock_guard lock(g_mutex); + Materialize(fd).closeOnExec = value; +} + +int RecordLock(int fd, int command, std::intptr_t argument) { + const bool old = command <= OldSetLockWait; + const int current = old ? command + (GetLock - OldGetLock) : command; + const std::size_t size = old ? OldGuestLockSize : sizeof(GuestLock); + auto* pointer = reinterpret_cast(argument); + if (pointer == nullptr) return Fail(GuestEfault); + GuestLock lock{}; + std::memcpy(&lock, pointer, size); + const auto kind = Classify(fd, "fcntl"); + if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); + if (*kind != Kind::File && *kind != Kind::Directory) Unsupported("fcntl", "a record lock", *kind, fd); +#ifdef _WIN32 + static_cast(current); + throw std::runtime_error("fcntl: record locks are not implemented on Windows, command=" + std::to_string(command) + ", fd=" + std::to_string(fd)); +#else + struct flock host{}; + if (lock.type == ReadLock) host.l_type = F_RDLCK; + else if (lock.type == WriteLock) host.l_type = F_WRLCK; + else if (lock.type == Unlock) host.l_type = F_UNLCK; + else return Fail(GuestEinval); + if (current == GetLock && lock.type == Unlock) + throw std::runtime_error("fcntl: F_GETLK with F_UNLCK is not implemented, fd=" + std::to_string(fd)); + host.l_whence = lock.whence; + host.l_start = static_cast(lock.start); + host.l_len = static_cast(lock.length); + const int hostCommand = current == GetLock ? F_GETLK : current == SetLock ? F_SETLK : F_SETLKW; + if (::fcntl(fd, hostCommand, &host) != 0) return Fail(LockError(errno)); + if (current != GetLock) return 0; + if (host.l_type == F_UNLCK) { + if (lock.whence == FromCurrent) { + const auto offset = ::lseek(fd, 0, SEEK_CUR); + if (offset < 0) throw std::runtime_error("fcntl: cannot read the offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + lock.start += offset; + } + lock.type = Unlock; + } else { + lock.type = host.l_type == F_RDLCK ? ReadLock : WriteLock; + lock.whence = SEEK_SET; + lock.start = host.l_start; + lock.length = host.l_len; + lock.pid = host.l_pid; + lock.system = 0; + } + const GuestArena::HostWrite destination(pointer, size); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(pointer, &lock, size); + return 0; +#endif +} + +int ReadAheadControl(int fd, int command, std::intptr_t argument) { + const auto kind = Classify(fd, "fcntl"); + if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); + if (*kind != Kind::File) return Fail(GuestEnotty); + std::lock_guard lock(g_mutex); + auto& status = *Materialize(fd).status; + if (command == DarwinReadAhead || argument >= 0) status |= ReadAheadFlag; + else status &= ~ReadAheadFlag; + return 0; +} + +} + +namespace File { + +int Fcntl(int fd, int command, std::intptr_t argument) { + switch (command) { + case DuplicateCommand: + case DuplicateCloseOnExec: { + if (fd < 0) return Fail(GuestEbadf); + if (static_cast(argument) < 0) return Fail(GuestEinval); + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + const int duplicate = DuplicateFrom(fd, static_cast(argument)); + if (duplicate >= 0 && command == DuplicateCloseOnExec) SetCloseOnExec(duplicate, true); + return duplicate; + } + case DuplicateToCommand: + case DuplicateToCloseOnExec: { + const int duplicate = dup2_nid_postfix(fd, static_cast(argument)); + if (duplicate >= 0 && command == DuplicateToCloseOnExec) SetCloseOnExec(duplicate, true); + return duplicate; + } + case GetDescriptorFlags: { + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + std::lock_guard lock(g_mutex); + return Materialize(fd).closeOnExec ? CloseOnExec : 0; + } + case SetDescriptorFlags: + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + SetCloseOnExec(fd, (argument & CloseOnExec) != 0); + return 0; + case GetStatusFlags: { + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + const int access = AccessMode(fd, "fcntl"); + return access | CurrentStatus(fd); + } + case SetStatusFlags: { + const auto kind = Classify(fd, "fcntl"); + if (!kind) return Fail(GuestEbadf); + return ChangeStatus(fd, *kind, SettableStatusFlags, static_cast(argument), "fcntl"); + } + case GetOwner: + case SetOwner: { + const auto kind = Classify(fd, "fcntl"); + if (!kind) return Fail(GuestEbadf); + Unsupported("fcntl", command == GetOwner ? "F_GETOWN" : "F_SETOWN", *kind, fd); + } + case OldGetLock: + case OldSetLock: + case OldSetLockWait: + case GetLock: + case SetLock: + case SetLockWait: + return RecordLock(fd, command, argument); + case SetLockRemote: + throw std::runtime_error("fcntl: F_SETLK_REMOTE needs a privilege whose state on the console is not known, fd=" + std::to_string(fd)); + case ReadAhead: + case DarwinReadAhead: + return ReadAheadControl(fd, command, argument); + default: + return Fail(GuestEinval); + } +} + +void RememberDescriptor(int fd, int openFlags) { + std::lock_guard lock(g_mutex); + g_entries[fd] = Entry{(openFlags & OpenCloseOnExec) != 0, std::make_shared(openFlags & OpenStatusFlags)}; +} + +void ShareDescriptor(int from, int to) { + std::lock_guard lock(g_mutex); + auto status = Materialize(from).status; + g_entries[to] = Entry{false, std::move(status)}; +} + +void ForgetDescriptor(int fd) { + std::lock_guard lock(g_mutex); + g_entries.erase(fd); +} + +} diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index caae32d96..f8a287b67 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -6,6 +6,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" @@ -80,14 +81,19 @@ static int NativeRead(int fd, void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelRead: nbytes exceeds platform limit"); } - return ::_read(fd, buf, static_cast(n)); + const int result = ::_read(fd, buf, static_cast(n)); + if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; + return result; } static int NativeWrite(int fd, const void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelWrite: nbytes exceeds platform limit"); } - const auto result = File::WriteSharedMemory_nid_no_patch(fd, buf, n); - return result == -2 ? ::_write(fd, buf, static_cast(n)) : static_cast(result); + const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n); + if (shared != -2) return static_cast(shared); + const int result = ::_write(fd, buf, static_cast(n)); + if (result < 0 && errno == ENOSPC && _doserrno == 0 && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EAGAIN; + return result; } extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); static void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} @@ -209,6 +215,7 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 } if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC))) RecordWrittenPath_nid_no_patch(native); + File::RememberDescriptor(fd, flags); return fd; } @@ -220,7 +227,9 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { if (path != nullptr && File::IsRandomDevicePath(path)) { if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY) throw std::runtime_error(std::string(__func__) + ": writing to " + path + " is not implemented"); const int fd = File::OpenRandomDevice(); - return fd < 0 ? SceErrorFromErrno(errno) : fd; + if (fd < 0) return SceErrorFromErrno(errno); + File::RememberDescriptor(fd, flags); + return fd; } std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); @@ -236,6 +245,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std int APS5_VABI sceKernelClose(int d) { File::ForgetRandomDevice(d); + File::ForgetDescriptor(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); @@ -332,27 +342,21 @@ int APS5_VABI sceKernelUnlink(const char* path) { } int APS5_VABI sceKernelFcntl(int d, int command, ...) { - if (d < GuestSockets::FirstDescriptor) { - if (!DescriptorIsOpen_nid_no_patch(d)) return SCE_KERNEL_ERROR_EBADF; - throw std::runtime_error(std::string(__func__) + ": file descriptors are not implemented, fd=" + std::to_string(d)); - } - int result; - if (command == GuestSetFlags) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const auto argument = __builtin_va_arg(arguments, long long); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - const int flags = va_arg(arguments, int); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + const auto argument = va_arg(arguments, long); + va_end(arguments); #endif - result = fcntl_nid_postfix(d, command, flags); - } else { - result = fcntl_nid_postfix(d, command); - } + int result; + if (d < GuestSockets::FirstDescriptor) result = File::Fcntl(d, command, static_cast(argument)); + else if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); + else result = fcntl_nid_postfix(d, command); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index fb0fef9e6..ab343dadf 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -12,6 +12,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/File/include/KernelStatfs.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -410,6 +411,8 @@ extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors) { const int result = ::pipe(native); #endif if (result != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + File::RememberDescriptor(native[0], 0); + File::RememberDescriptor(native[1], 0); std::memcpy(descriptors, native, sizeof(native)); return 0; } @@ -454,6 +457,28 @@ static bool HasNonDirectoryPrefix(const std::filesystem::path& native) { return false; } +extern "C" int APS5_VABI dup_nid_postfix(int d); +extern "C" int APS5_VABI close_nid_postfix(int d); + +int File::DuplicateFrom(int fd, int start) { +#ifdef _WIN32 + std::vector below; + int duplicate; + while ((duplicate = dup_nid_postfix(fd)) >= 0 && duplicate < start) below.push_back(duplicate); + const int error = *__error_nid_postfix(); + for (const int descriptor : below) close_nid_postfix(descriptor); + return duplicate < 0 ? PosixFailure(error) : duplicate; +#else + const int duplicate = ::fcntl(fd, F_DUPFD, start); + if (duplicate < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + if (duplicate >= GuestSockets::FirstDescriptor) + throw std::runtime_error("fcntl: host descriptor reached the guest socket range"); + if (File::IsRandomDevice(fd)) File::RememberRandomDevice(duplicate); + File::ShareDescriptor(fd, duplicate); + return duplicate; +#endif +} + extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { @@ -467,6 +492,7 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { int APS5_VABI close_nid_postfix(int d) { if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d); File::ForgetRandomDevice(d); + File::ForgetDescriptor(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); @@ -493,6 +519,7 @@ int APS5_VABI dup_nid_postfix(int d) { if (duplicate >= GuestSockets::FirstDescriptor) throw std::runtime_error(std::string(__func__) + ": host descriptor reached the guest socket range"); if (File::IsRandomDevice(d)) File::RememberRandomDevice(duplicate); + File::ShareDescriptor(d, duplicate); return duplicate; } @@ -517,6 +544,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { File::ForgetFileLock(to); File::ForgetRandomDevice(to); if (sourceIsRandom) File::RememberRandomDevice(to); + File::ShareDescriptor(from, to); return to; #else if (from == to) { @@ -527,6 +555,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (result < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); File::ForgetRandomDevice(to); if (sourceIsRandom) File::RememberRandomDevice(to); + File::ShareDescriptor(from, to); return result; #endif } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 436c88e28..03892b0de 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -17,6 +17,7 @@ #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include @@ -435,6 +436,20 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { + if (descriptor < GuestSockets::FirstDescriptor) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const auto argument = __builtin_va_arg(arguments, long long); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, command); + const auto argument = va_arg(arguments, long); + va_end(arguments); +#endif + return File::Fcntl(descriptor, command, static_cast(argument)); + } const auto socket = Lookup(descriptor); if (!socket) return -1; std::lock_guard lock(socket->modeMutex); diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 1388a600d..e4241dc52 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -15,6 +15,7 @@ int APS5_VABI socketpair_nid_postfix(int, int, int, int*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); } @@ -178,10 +179,39 @@ static void RandomDeviceDuplicates() { for (const int descriptor : {random, dupRandom, pipe[1], secondRandom}) Require(close_nid_postfix(descriptor) == 0); } +static void DuplicatesShareStatusFlags() { + constexpr int GetDescriptorFlags = 1; + constexpr int SetDescriptorFlags = 2; + constexpr int GetFlags = 3; + constexpr int SetFlags = 4; + constexpr int NonBlocking = 4; + constexpr int GuestEagain = 35; + int pipe[2]; + int other[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(pipe_nid_postfix(other) == 0); + Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + const int duplicate = dup_nid_postfix(pipe[0]); + Require(duplicate >= 0); + Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(duplicate, SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == NonBlocking); + Require(dup2_nid_postfix(pipe[0], other[0]) == other[0]); + Require(fcntl_nid_postfix(other[0], GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(other[0], GetFlags) == NonBlocking); + char buffer[4] = {}; + RequireFailure(static_cast(read_nid_postfix(other[0], buffer, sizeof(buffer))), GuestEagain); + Require(fcntl_nid_postfix(pipe[0], SetFlags, 0) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == 0); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 1); + for (const int descriptor : {pipe[0], pipe[1], other[0], other[1], duplicate}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { DupOutlivesOriginal(); Dup2ReplacesTarget(); BadDescriptors(); SocketDuplicates(); RandomDeviceDuplicates(); + DuplicatesShareStatusFlags(); } diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 0c80bb893..834b38236 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -9,19 +9,68 @@ extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelFcntl(int, int, ...); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); } static void Require(bool value) { if (!value) std::abort(); } +static constexpr int DuplicateCommand = 0; +static constexpr int GetDescriptorFlags = 1; +static constexpr int SetDescriptorFlags = 2; static constexpr int GetFlags = 3; static constexpr int SetFlags = 4; +static constexpr int GetLock = 11; +static constexpr int SetLock = 12; +static constexpr int DarwinReadAhead = 16; +static constexpr int DuplicateCloseOnExec = 17; +static constexpr int ReadOnly = 0; +static constexpr int WriteOnly = 1; static constexpr int ReadWrite = 2; static constexpr int NonBlocking = 4; +static constexpr int Append = 8; +static constexpr int ReadAheadFlag = 0x200; +static constexpr int CreateReadWrite = 0x202; +static constexpr int CreateWriteAppend = 0x209; +static constexpr int GuestEbadf = 9; +static constexpr int GuestEfault = 14; +static constexpr int GuestEinval = 22; +static constexpr int GuestEagain = 35; +static constexpr std::uint64_t ReadableRequest = 0x4004667f; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); -int main() { + +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + +template static bool ThrowsWith(TCall call, const std::string& prefix) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()).rfind(prefix, 0) == 0; + } + return false; +} + +static void SocketsKeepTheirPath() { const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); @@ -33,21 +82,130 @@ int main() { Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); Require(sceKernelFcntl(socket, 5) == SceEinval); Require(sceKernelFcntl(socket + 4096, GetFlags) == SceEbadf); + int queued = -1; + Require(ioctl_nid_postfix(socket, ReadableRequest, &queued) == 0 && queued == 0); Require(close_nid_postfix(socket) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); - const int file = sceKernelOpen("kernel_fcntl_probe.bin", 0x202, 0644); +} + +static void FileFlagsRoundTrip() { + const int file = sceKernelOpen("kernel_fcntl_probe.bin", CreateReadWrite, 0644); Require(file >= 0); - bool rejected = false; - try { - sceKernelFcntl(file, GetFlags); - } catch (const std::runtime_error& error) { - rejected = std::string(error.what()).rfind("sceKernelFcntl: file descriptors are not implemented", 0) == 0; - } - Require(rejected); - Require(sceKernelClose(file) == 0); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, SetFlags, ReadWrite | NonBlocking | 0x8000) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | NonBlocking)); + Require(fcntl_nid_postfix(file, SetFlags, 0) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(file, SetDescriptorFlags, 0) == 0); + Require(sceKernelFcntl(file, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + + const int duplicate = fcntl_nid_postfix(file, DuplicateCommand, 20); + Require(duplicate >= 20); + Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == (ReadWrite | NonBlocking)); + const int closing = fcntl_nid_postfix(file, DuplicateCloseOnExec, duplicate); + Require(closing > duplicate); + Require(fcntl_nid_postfix(closing, GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); + + Require(write_nid_postfix(file, "abcde", 5) == 5); + Require(sceKernelLseek(file, 2, 0) == 2); + + RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, -1), GuestEinval); + Require(sceKernelFcntl(file, DuplicateCommand, -1) == SceEinval); + RequireFailure(fcntl_nid_postfix(file, 99), GuestEinval); + RequireFailure(fcntl_nid_postfix(file, GetLock, nullptr), GuestEfault); +#ifdef _WIN32 + GuestLock lock{0, 0, 0, 3, 0, 0}; + Require(ThrowsWith([file, &lock] { fcntl_nid_postfix(file, SetLock, &lock); }, "fcntl: record locks are not implemented on Windows")); +#else + GuestLock lock{0, 0, 0, 3, 0, 0}; + Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); + GuestLock probe{0, 0, 0, 1, 0, 0}; + Require(fcntl_nid_postfix(file, GetLock, &probe) == 0 && probe.type == 2); + lock.type = 2; + Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); + lock.type = 7; + RequireFailure(fcntl_nid_postfix(file, SetLock, &lock), GuestEinval); +#endif + + for (const int descriptor : {closing, duplicate, file}) Require(close_nid_postfix(descriptor) == 0); + RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); + RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, 0), GuestEbadf); Require(sceKernelFcntl(file, GetFlags) == SceEbadf); + + const int appending = sceKernelOpen("kernel_fcntl_probe.bin", CreateWriteAppend, 0644); + Require(appending >= 0); + Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append)); + Require(fcntl_nid_postfix(appending, SetFlags, Append | NonBlocking) == 0); + Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append | NonBlocking)); +#ifdef _WIN32 + Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, 0); }, "fcntl: changing O_APPEND on a file is not implemented")); +#else + Require(fcntl_nid_postfix(appending, SetFlags, 0) == 0); + Require(fcntl_nid_postfix(appending, GetFlags) == WriteOnly); +#endif + Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, Append | 0x80); }, "fcntl: changing O_FSYNC on a file is not implemented")); + Require(sceKernelClose(appending) == 0); std::remove("kernel_fcntl_probe.bin"); +} + +static void PipeFlagsRoundTrip() { + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + Require(fcntl_nid_postfix(pipe[1], GetFlags) == WriteOnly); + Require(fcntl_nid_postfix(pipe[1], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 0); + + Require(fcntl_nid_postfix(pipe[0], SetFlags, fcntl_nid_postfix(pipe[0], GetFlags) | NonBlocking) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + char buffer[8] = {}; + RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + + Require(fcntl_nid_postfix(pipe[1], SetFlags, NonBlocking) == 0); + char block[1024] = {}; + std::int64_t written = 0; + std::int64_t result = 0; + while (written < (1 << 22) && (result = write_nid_postfix(pipe[1], block, sizeof(block))) > 0) written += result; + Require(written > 0); + RequireFailure(static_cast(result), GuestEagain); + + RequireFailure(fcntl_nid_postfix(pipe[0], DarwinReadAhead, 1), GuestEbadf); + GuestLock lock{0, 0, 0, 3, 0, 0}; + RequireFailure(fcntl_nid_postfix(pipe[1], SetLock, &lock), GuestEbadf); + Require(ThrowsWith([&pipe] { fcntl_nid_postfix(pipe[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a pipe is not implemented")); + for (const int descriptor : pipe) Require(close_nid_postfix(descriptor) == 0); +} + +static void StandardInputFlags() { + const int flags = fcntl_nid_postfix(0, GetFlags); + Require(flags >= 0); + Require((flags & 3) != WriteOnly); + Require(fcntl_nid_postfix(0, SetFlags, fcntl_nid_postfix(0, GetFlags) & ~NonBlocking) == 0); + Require(fcntl_nid_postfix(0, GetFlags) == (flags & ~NonBlocking)); + Require(fcntl_nid_postfix(0, SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(0, GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(0, SetDescriptorFlags, 0) == 0); + Require(fcntl_nid_postfix(0, GetDescriptorFlags) == 0); +} + +int main() { + SocketsKeepTheirPath(); + FileFlagsRoundTrip(); + PipeFlagsRoundTrip(); + StandardInputFlags(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f42d54e58..3514a6021 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -652,7 +652,8 @@ Throughout the project, every function at every stage either **does exactly what - [getsid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: process sessions are not modelled. PPSA74126 imports it and fails to load without it; the title made no call to it in a 60 s run - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` and `kill` of the other signals still fail with `EINVAL`, recorded handler or not - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported -- [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. The return values and error codes are unverified on a console +- [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console +- [fcntl](../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and throw on Windows, and `F_GETLK` with `F_UNLCK` throws. The return values and error codes are unverified on a console - [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket. Any other request on a socket fails with `ENOTTY`, and any request on a file or pipe descriptor fails with `EBADF`, where FreeBSD answers `FIONBIO` and `FIONREAD` there too and fails other requests, termios ones included, with `ENOTTY`. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [sendfile](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread From 01b7bb3e2f2568a263fc014eabfc199a5452d466 Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 18:01:27 +0200 Subject: [PATCH 265/318] feat(libkernel): support ioctl on non-socket descriptors ioctl and _ioctl answered EBADF for every descriptor outside the libSceNet socket table. Decoded call sites: PS5MC (PPSA99420, FIONBIO x3, FIONREAD x2, TIOCGWINSZ x3), ProsperoTV (PPSA99003, FIONBIO on a pipe once poll on pipes is in), XPSemu (PPSA97358, 21 sites). Following FreeBSD 11.4 kern_ioctl (sys/kern/sys_generic.c:758), vn_ioctl (sys/kern/vfs_vnops.c:1488), pipe_ioctl (sys/kern/sys_pipe.c:1346) and randomdev_ioctl (sys/dev/random/randomdev.c:301), release/11.4.0 8c639945: FIONBIO sets or clears O_NONBLOCK through the same status flags as fcntl F_SETFL (EFAULT for a null argument before EBADF); FIONREAD reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end, ENOTTY on the random device, and throws on a directory or another character device; termios requests (group 't', TIOCGWINSZ included) fail with ENOTTY, as tcgetattr already takes these descriptors as not a terminal; any other request throws naming the request and descriptor kind. Sockets keep their path unchanged. Co-Authored-By: Claude Fable 5.1 --- .../File/include/DescriptorControl.hpp | 1 + .../libkernel/File/src/DescriptorControl.cpp | 64 +++++++++++++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 1 + core/libs/tests/GuestKernelFcntl.cpp | 23 +++++++ docs/dev/TechnicalDebt.md | 2 +- 5 files changed, 90 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 195fa099d..a0969b442 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -6,6 +6,7 @@ namespace File { int Fcntl(int fd, int command, std::intptr_t argument); +int Ioctl(int fd, std::uint64_t request, void* argument); int DuplicateFrom(int fd, int start); void RememberDescriptor(int fd, int openFlags); void ShareDescriptor(int from, int to); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index ee8b4e0d0..847a315b7 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #else #include +#include #include #include #endif @@ -83,6 +85,11 @@ constexpr std::int16_t Unlock = 2; constexpr std::int16_t WriteLock = 3; constexpr std::int16_t FromCurrent = 1; +constexpr std::uint32_t NonblockingRequest = 0x8004667e; +constexpr std::uint32_t ReadableRequest = 0x4004667f; +constexpr std::uint32_t RequestIn = 0x80000000u; +constexpr std::uint32_t TerminalGroup = 't'; + struct GuestLock { std::int64_t start; std::int64_t length; @@ -187,6 +194,23 @@ void ApplyHostStatus(int fd, Kind kind, int changed, int next, const char* funct if ((changed & Nonblocking) && kind == Kind::Pipe) SetPipeNonblocking(fd, (next & Nonblocking) != 0, function); } +int Readable(int fd, Kind kind, const char* function) { + if (kind == Kind::File) { + const auto size = ::_filelengthi64(fd); + const auto offset = ::_telli64(fd); + if (size < 0 || offset < 0) + throw std::runtime_error(std::string(function) + ": cannot read the size and offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return static_cast(size - offset); + } + if (AccessMode(fd, function) == WriteOnly) return 0; + DWORD available = 0; + if (!::PeekNamedPipe(NativeHandle(fd), nullptr, 0, nullptr, &available, nullptr)) { + const DWORD error = ::GetLastError(); + if (error == ERROR_BROKEN_PIPE) return 0; + throw std::runtime_error(std::string(function) + ": cannot peek pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(error)); + } + return static_cast(available); +} #else std::optional Classify(int fd, const char* function) { if (fd < 0) return std::nullopt; @@ -234,6 +258,20 @@ void ApplyHostStatus(int fd, Kind, int changed, int next, const char* function) throw std::runtime_error(std::string(function) + ": host F_SETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); } +int Readable(int fd, Kind kind, const char* function) { + if (kind == Kind::File) { + struct stat status{}; + const auto offset = ::lseek(fd, 0, SEEK_CUR); + if (::fstat(fd, &status) != 0 || offset < 0) + throw std::runtime_error(std::string(function) + ": cannot read the size and offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return static_cast(status.st_size - offset); + } + if (AccessMode(fd, function) == WriteOnly) return 0; + int available = 0; + if (::ioctl(fd, FIONREAD, &available) != 0) + throw std::runtime_error(std::string(function) + ": host FIONREAD failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return available; +} int LockError(int error) { switch (error) { @@ -402,6 +440,32 @@ int Fcntl(int fd, int command, std::intptr_t argument) { } } +int Ioctl(int fd, std::uint64_t request, void* argument) { + const auto command = static_cast(request); + const std::uint32_t length = (command >> 16) & 0x1fff; + if ((command & RequestIn) && length != 0 && argument == nullptr) return Fail(GuestEfault); + const auto kind = Classify(fd, "ioctl"); + if (!kind) return Fail(GuestEbadf); + if (command == NonblockingRequest) { + const bool enable = *static_cast(argument) != 0; + return ChangeStatus(fd, *kind, Nonblocking, enable ? Nonblocking : 0, "ioctl"); + } + if (command == ReadableRequest) { + if (*kind == Kind::RandomDevice) return Fail(GuestEnotty); + if (*kind != Kind::File && *kind != Kind::Pipe) Unsupported("ioctl", "FIONREAD", *kind, fd); + const int available = Readable(fd, *kind, "ioctl"); + if (argument == nullptr) return Fail(GuestEfault); + const GuestArena::HostWrite destination(argument, sizeof(available)); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(argument, &available, sizeof(available)); + return 0; + } + if (((command >> 8) & 0xff) == TerminalGroup) return Fail(GuestEnotty); + char hex[11]; + std::snprintf(hex, sizeof(hex), "0x%08X", command); + Unsupported("ioctl", std::string("request ") + hex, *kind, fd); +} + void RememberDescriptor(int fd, int openFlags) { std::lock_guard lock(g_mutex); g_entries[fd] = Entry{(openFlags & OpenCloseOnExec) != 0, std::make_shared(openFlags & OpenStatusFlags)}; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 03892b0de..b8923bd35 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -665,6 +665,7 @@ int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32 return 0; } int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { + if (descriptor < GuestSockets::FirstDescriptor) return File::Ioctl(descriptor, request, argument); const auto socket = Lookup(descriptor); if (!socket) return -1; if (!argument) return Fail(14); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 834b38236..c46f6a711 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -41,8 +41,12 @@ static constexpr int CreateWriteAppend = 0x209; static constexpr int GuestEbadf = 9; static constexpr int GuestEfault = 14; static constexpr int GuestEinval = 22; +static constexpr int GuestEnotty = 25; static constexpr int GuestEagain = 35; +static constexpr std::uint64_t NonblockingRequest = 0x8004667e; static constexpr std::uint64_t ReadableRequest = 0x4004667f; +static constexpr std::uint64_t WindowSizeRequest = 0x40087468; +static constexpr std::uint64_t AsyncRequest = 0x8004667d; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); @@ -118,6 +122,12 @@ static void FileFlagsRoundTrip() { Require(write_nid_postfix(file, "abcde", 5) == 5); Require(sceKernelLseek(file, 2, 0) == 2); + int readable = -1; + Require(ioctl_nid_postfix(file, ReadableRequest, &readable) == 0 && readable == 3); + char size[8] = {}; + RequireFailure(ioctl_nid_postfix(file, WindowSizeRequest, size), GuestEnotty); + RequireFailure(ioctl_nid_postfix(file, NonblockingRequest, nullptr), GuestEfault); + Require(ThrowsWith([file] { int on = 1; ioctl_nid_postfix(file, AsyncRequest, &on); }, "ioctl: request 0x8004667D on a file is not implemented")); RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, -1), GuestEinval); Require(sceKernelFcntl(file, DuplicateCommand, -1) == SceEinval); @@ -140,6 +150,7 @@ static void FileFlagsRoundTrip() { for (const int descriptor : {closing, duplicate, file}) Require(close_nid_postfix(descriptor) == 0); RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, 0), GuestEbadf); + RequireFailure(ioctl_nid_postfix(file, ReadableRequest, &readable), GuestEbadf); Require(sceKernelFcntl(file, GetFlags) == SceEbadf); const int appending = sceKernelOpen("kernel_fcntl_probe.bin", CreateWriteAppend, 0644); @@ -172,9 +183,19 @@ static void PipeFlagsRoundTrip() { char buffer[8] = {}; RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + int readable = -1; + Require(ioctl_nid_postfix(pipe[0], ReadableRequest, &readable) == 0 && readable == 3); + Require(ioctl_nid_postfix(pipe[1], ReadableRequest, &readable) == 0 && readable == 0); Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + int off = 0; + Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &off) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + int on = 1; + Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &on) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + Require(fcntl_nid_postfix(pipe[1], SetFlags, NonBlocking) == 0); char block[1024] = {}; std::int64_t written = 0; @@ -186,6 +207,8 @@ static void PipeFlagsRoundTrip() { RequireFailure(fcntl_nid_postfix(pipe[0], DarwinReadAhead, 1), GuestEbadf); GuestLock lock{0, 0, 0, 3, 0, 0}; RequireFailure(fcntl_nid_postfix(pipe[1], SetLock, &lock), GuestEbadf); + char size[8] = {}; + RequireFailure(ioctl_nid_postfix(pipe[0], WindowSizeRequest, size), GuestEnotty); Require(ThrowsWith([&pipe] { fcntl_nid_postfix(pipe[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a pipe is not implemented")); for (const int descriptor : pipe) Require(close_nid_postfix(descriptor) == 0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3514a6021..fe45a0e2b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -654,7 +654,7 @@ Throughout the project, every function at every stage either **does exactly what - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and throw on Windows, and `F_GETLK` with `F_UNLCK` throws. The return values and error codes are unverified on a console -- [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket. Any other request on a socket fails with `ENOTTY`, and any request on a file or pipe descriptor fails with `EBADF`, where FreeBSD answers `FIONBIO` and `FIONREAD` there too and fails other requests, termios ones included, with `ENOTTY`. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [sendfile](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. From 99b1b32f315d78730d2ed91b2f8d33b077a34fba Mon Sep 17 00:00:00 2001 From: Ahir <191682168+ahir061@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:10:01 +0000 Subject: [PATCH 266/318] fix(libkernel): read the fcntl argument by command fcntl and sceKernelFcntl read a third argument as a 64-bit integer for every command on a non-socket descriptor. F_GETFD and F_GETFL pass no third argument, and the commands that take one pass an int or a pointer. They now read nothing, an int or a pointer, by command. An int read as a 64-bit value kept whatever the caller left in the upper half of the register, so F_READAHEAD -1 enabled read-ahead instead of disabling it. --- .../File/include/DescriptorControl.hpp | 3 +++ .../libkernel/File/src/DescriptorControl.cpp | 25 +++++++++++++++++++ core/libs/prx/libkernel/File/src/Open.cpp | 24 +++++++++++------- core/libs/prx/libkernel/Socket/src/Udp.cpp | 24 +++++++++++------- core/libs/tests/GuestKernelFcntl.cpp | 8 ++++++ 5 files changed, 66 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index a0969b442..cb327dff5 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -5,6 +5,9 @@ namespace File { +enum class FcntlArgument { None, Int, Pointer }; + +FcntlArgument FcntlArgumentOf(int command); int Fcntl(int fd, int command, std::intptr_t argument); int Ioctl(int fd, std::uint64_t request, void* argument); int DuplicateFrom(int fd, int start); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 847a315b7..7bdd8c8bc 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -381,6 +381,31 @@ int ReadAheadControl(int fd, int command, std::intptr_t argument) { namespace File { +FcntlArgument FcntlArgumentOf(int command) { + switch (command) { + case DuplicateCommand: + case SetDescriptorFlags: + case SetStatusFlags: + case SetOwner: + case DuplicateToCommand: + case ReadAhead: + case DarwinReadAhead: + case DuplicateCloseOnExec: + case DuplicateToCloseOnExec: + return FcntlArgument::Int; + case OldGetLock: + case OldSetLock: + case OldSetLockWait: + case GetLock: + case SetLock: + case SetLockWait: + case SetLockRemote: + return FcntlArgument::Pointer; + default: + return FcntlArgument::None; + } +} + int Fcntl(int fd, int command, std::intptr_t argument) { switch (command) { case DuplicateCommand: diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index f8a287b67..4d0135c0f 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -342,19 +342,25 @@ int APS5_VABI sceKernelUnlink(const char* path) { } int APS5_VABI sceKernelFcntl(int d, int command, ...) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const auto argument = __builtin_va_arg(arguments, long long); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - const auto argument = va_arg(arguments, long); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); + va_end(arguments); #endif + } int result; - if (d < GuestSockets::FirstDescriptor) result = File::Fcntl(d, command, static_cast(argument)); + if (d < GuestSockets::FirstDescriptor) result = File::Fcntl(d, command, argument); else if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); else result = fcntl_nid_postfix(d, command); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b8923bd35..8f9420d8e 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -437,18 +437,24 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { if (descriptor < GuestSockets::FirstDescriptor) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const auto argument = __builtin_va_arg(arguments, long long); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - const auto argument = va_arg(arguments, long); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); + va_end(arguments); #endif - return File::Fcntl(descriptor, command, static_cast(argument)); + } + return File::Fcntl(descriptor, command, argument); } const auto socket = Lookup(descriptor); if (!socket) return -1; diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index c46f6a711..c2c6030b3 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -28,6 +28,7 @@ static constexpr int GetFlags = 3; static constexpr int SetFlags = 4; static constexpr int GetLock = 11; static constexpr int SetLock = 12; +static constexpr int ReadAhead = 15; static constexpr int DarwinReadAhead = 16; static constexpr int DuplicateCloseOnExec = 17; static constexpr int ReadOnly = 0; @@ -109,6 +110,13 @@ static void FileFlagsRoundTrip() { Require(sceKernelFcntl(file, GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + Require(fcntl_nid_postfix(file, ReadAhead, -1) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == ReadWrite); + Require(sceKernelFcntl(file, ReadAhead, 4096) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + Require(sceKernelFcntl(file, ReadAhead, -1) == 0); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); const int duplicate = fcntl_nid_postfix(file, DuplicateCommand, 20); Require(duplicate >= 20); From 9504762edbb2659acbf5a24d13a0bec6ab1137fc Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 12 Oct 2026 00:00:28 -0700 Subject: [PATCH 267/318] fix(tools): ignore unreachable CTest registration files --- tools/check_test_registration.py | 15 ++++++++--- tools/tests/test_test_registration.py | 39 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 83311cb20..0fb47521a 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -4,7 +4,7 @@ import subprocess from pathlib import Path TEST_OUTPUT = r"tests[\\/]" -COMMAND = re.compile(r"^\s*(add_test|set_tests_properties)\s*\(", re.M | re.I) +COMMAND = re.compile(r"^\s*(add_test|set_tests_properties|subdirs)\s*\(", re.M | re.I) ARGUMENT = re.compile(r'"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P=equals)\]|[^\s()]+', re.S) KNOWN_UNRUN = set() @@ -63,10 +63,19 @@ def commands(content): def registered(build): names = set() - for file in Path(build).rglob("CTestTestfile.cmake"): + pending = [Path(build).resolve()] + visited = set() + while pending: + directory = pending.pop().resolve() + file = directory / "CTestTestfile.cmake" + if directory in visited or not file.is_file(): + continue + visited.add(directory) tests, disabled = {}, {} for command, arguments in commands(file.read_text(encoding="utf-8", errors="replace")): - if command == "add_test" and arguments: + if command == "subdirs": + pending.extend(directory / argument for argument in arguments) + elif command == "add_test" and arguments: tests[arguments[0]] = arguments[1:] elif command == "set_tests_properties" and "PROPERTIES" in arguments: properties = arguments.index("PROPERTIES") diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index 5a4649281..3d91bf0d6 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -80,6 +80,45 @@ class RegistrationTests(unittest.TestCase): names = self.registered(r'add_test("bounds" "C:\\build\\tests\\buffer_bounds_tests.exe")' + '\n') self.assertIn("buffer_bounds_tests", names) + def test_unreferenced_registration_directory_does_not_count(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CTestTestfile.cmake").write_text("", encoding="utf-8") + stale = build / "removed" + stale.mkdir() + (stale / "CTestTestfile.cmake").write_text( + 'add_test("bounds" "/build/tests/buffer_bounds_tests")\n', encoding="utf-8") + self.assertNotIn("buffer_bounds_tests", check_test_registration.registered(build)) + (build / "CTestTestfile.cmake").unlink() + self.assertNotIn("buffer_bounds_tests", check_test_registration.registered(build)) + + def test_only_reachable_nested_directories_are_registered(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + active = build / "active tests" + nested = active / "nested" + removed = active / "removed" + nested.mkdir(parents=True) + removed.mkdir() + (build / "CTestTestfile.cmake").write_text('subdirs("active tests")\n', encoding="utf-8") + (active / "CTestTestfile.cmake").write_text('subdirs([=[nested]=])\n', encoding="utf-8") + for path, name in ((nested, "active_tests"), (removed, "removed_tests")): + (path / "CTestTestfile.cmake").write_text( + f'add_test("check" "/build/tests/{name}")\n', encoding="utf-8") + self.assertEqual(check_test_registration.registered(build), {"active_tests"}) + + def test_external_registration_directory_is_followed(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + build, external = root / "build", root / "external" + build.mkdir() + external.mkdir() + (build / "CTestTestfile.cmake").write_text( + f'subdirs("{external.as_posix()}")\n', encoding="utf-8") + (external / "CTestTestfile.cmake").write_text( + 'add_test("bounds" "/build/tests/buffer_bounds_tests")\n', encoding="utf-8") + self.assertEqual(check_test_registration.registered(build), {"buffer_bounds_tests"}) + if __name__ == "__main__": unittest.main() From 341dafb7cadb6dfc18a716f0546a5504a93f57c1 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 12 Oct 2026 00:00:52 -0700 Subject: [PATCH 268/318] fix(relinker): rebase each origin in guest runpath lists --- .../relinker/src/guest/GuestModuleBuilder.cpp | 11 +++++++---- core/relinker/relinker/tests/test_guest_runpath.py | 10 +++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index f6c8f8a49..f3d2d5336 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -331,7 +331,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); - if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = std::filesystem::absolute(outputPath).parent_path().lexically_normal(); std::vector artifacts; for (const auto index : order) { @@ -370,9 +370,12 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); else { std::string guestRunPath = runPath; - if (runPath == "$ORIGIN" || runPath.starts_with("$ORIGIN/")) { - const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); - guestRunPath = "$ORIGIN/" + relativeRoot + runPath.substr(7); + const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); + for (std::size_t position = 0; (position = guestRunPath.find("$ORIGIN", position)) != std::string::npos;) { + position += 7; + if (position != guestRunPath.size() && guestRunPath[position] != '/' && guestRunPath[position] != ':') continue; + guestRunPath.insert(position, "/" + relativeRoot); + position += relativeRoot.size() + 1; } std::vector needed; for (const auto dependency : dependencies[index]) { diff --git a/core/relinker/relinker/tests/test_guest_runpath.py b/core/relinker/relinker/tests/test_guest_runpath.py index 421199eda..754fee90a 100644 --- a/core/relinker/relinker/tests/test_guest_runpath.py +++ b/core/relinker/relinker/tests/test_guest_runpath.py @@ -17,7 +17,9 @@ def main(): checked = 0 for index, module_directory in enumerate(("sce_module", "prx")): for label, run_path in (("default", None), ("origin", "$ORIGIN"), - ("custom", "$ORIGIN/host libs"), ("absolute", str(work / "absolute libs"))): + ("custom", "$ORIGIN/host libs"), ("absolute", str(work / "absolute libs")), + ("list", "$ORIGIN/first:$ORIGIN/second"), + ("list-bare", "$ORIGIN:$ORIGIN/libs:$ORIGIN")): case = work / f"{index}-{label}" source = case / "input" / "input.elf" source.parent.mkdir(parents=True) @@ -41,8 +43,10 @@ def main(): assert 0 <= offset < len(strings) and end >= 0, (tags, strings) actual = strings[offset:end].decode() configured = run_path if run_path is not None else "$ORIGIN/libs" - expected = Path(configured.replace("$ORIGIN", str(output.parent))).resolve() - resolved = Path(actual.replace("$ORIGIN", str(artifact.parent))).resolve() + configured_paths = configured.split(":") if label.startswith("list") else [configured] + actual_paths = actual.split(":") if label.startswith("list") else [actual] + expected = [Path(path.replace("$ORIGIN", str(output.parent))).resolve() for path in configured_paths] + resolved = [Path(path.replace("$ORIGIN", str(artifact.parent))).resolve() for path in actual_paths] if resolved != expected or (label == "absolute" and actual != run_path): failures.append((str(relative), label, actual, str(resolved), str(expected))) checked += 1 From e2c24cf038b9bb5f200a666b2b5fedf2fdf10ff9 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 12 Oct 2026 00:01:09 -0700 Subject: [PATCH 269/318] fix(relinker): accept trailing separators on the Windows origin path --- core/relinker/CMakeLists.txt | 3 ++ .../src/windows/WindowsEntryStubBuilder.cpp | 2 + .../relinker/tests/test_windows_runpath.py | 42 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 core/relinker/relinker/tests/test_windows_runpath.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d3bdf0051..bc5a3536d 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -381,6 +381,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_gui.py $) add_test(NAME windows_working_directory COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_working_directory.py $) + add_test(NAME windows_runpath COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_runpath.py $) + set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 83281042f..a5ea5b7e3 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -26,6 +26,8 @@ std::string normalizeRunPath(std::string path) { path = "."; if (path.starts_with("$ORIGIN\\")) path.erase(0, 8); + if (path.empty()) + path = "."; if (path.find('$') != std::string::npos) throw Domain::RelinkerException("Unsupported variable in Windows run path"); if (path.back() != '\\') diff --git a/core/relinker/relinker/tests/test_windows_runpath.py b/core/relinker/relinker/tests/test_windows_runpath.py new file mode 100644 index 000000000..44db3f9bd --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_runpath.py @@ -0,0 +1,42 @@ +from pathlib import Path +import os +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_windows_import_modules import executable, provider + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-windows-runpath-") as directory: + work = Path(directory) + seed = work / "seed" + (seed / "prx").mkdir(parents=True) + (seed / "prx" / "a.prx").write_bytes(provider(77)) + source = seed / "input.elf" + source.write_bytes(main_fixture()) + result = subprocess.run([str(relinker), "--windows", str(source), str(seed / "output.exe")], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + library = (seed / "app0" / "prx" / "a.prx.guest.prx").read_bytes() + + for index, runpath in enumerate(("$ORIGIN", "$ORIGIN/", "$ORIGIN\\", "$ORIGIN/.", ".")): + case = work / str(index) + case.mkdir() + (case / "a.prx").write_bytes(library) + source = case / "input.elf" + source.write_bytes(executable("a.prx")) + output = case / "output.exe" + result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", "--rpath", runpath, + str(source), str(output)], capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (runpath, result.stdout, result.stderr) + if os.name == "nt": + executed = subprocess.run([str(output)], cwd=work, capture_output=True, text=True, timeout=20) + assert executed.returncode == 77, (runpath, executed.returncode, executed.stdout, executed.stderr) + print("Windows runpath tests passed") + + +if __name__ == "__main__": + main() From 4b6bb2f05928d8026d89459b22d3cba0a532ac6d Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 12 Oct 2026 00:01:25 -0700 Subject: [PATCH 270/318] fix(relinker): preserve inputs when writing Windows diagnostics --- core/relinker/main.cpp | 2 + .../relinker/tests/test_input_preservation.py | 67 +++++++++++++++++++ docs/user/USAGE.md | 2 +- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 0e747853d..4a7b7a7da 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -127,6 +127,8 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(desktopEntry->IconPath, "Icon"); checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); } + if (args.windowsDiagnostics) + checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics"); if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 2114abbe3..7863f55a7 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -8,6 +8,72 @@ from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture from test_linux_desktop_entry_output import png +def check_diagnostic_inputs(relinker, work): + filename = "windows-diagnostics-imports.txt" + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + for registry in (False, True): + case = work / f"diagnostics-{bundled}-{alias}-{registry}" + modules = case / "sce_module" + modules.mkdir(parents=True) + diagnostics = (modules if bundled and alias == "direct" else case) / filename + source = diagnostics if not bundled and alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + module = diagnostics if bundled and alias == "direct" else modules / "sample.prx" + module_original = bytes(guest_fixture(PLAIN_SITE)) + module.write_bytes(module_original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, diagnostics) + elif alias == "symlink": + try: + diagnostics.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Diagnostic symbolic-link case skipped: Windows symlink privilege unavailable") + continue + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registry_path = case / "output.registry.json" + previous_registry = b"previous registry" + registry_path.write_bytes(previous_registry) + options = [] if bundled else ["--skip-sce-module"] + if registry: + options.append("--registry") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", *options, + str(source), str(output)], cwd=diagnostics.parent, + capture_output=True, text=True, timeout=20) + owner = "an input module" if bundled else "the input executable" + assert result.returncode == 2 and f"Windows import diagnostics output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) + assert str(diagnostics) in result.stderr, result.stderr + assert source.read_bytes() == original and module.read_bytes() == module_original + assert diagnostics.read_bytes() == (module_original if bundled else original) + assert output.read_bytes() == previous_output and registry_path.read_bytes() == previous_registry + assert not (case / "app0").exists() + + for enabled in (False, True): + for existing in (False, True): + case = work / f"diagnostics-valid-{enabled}-{existing}" + case.mkdir() + diagnostics = case / filename + source = case / "input.elf" if enabled else diagnostics + original = bytes(main_fixture()) + source.write_bytes(original) + if enabled and existing: + diagnostics.write_bytes(b"previous diagnostics") + output = case / "output.exe" + options = ["--windows-diagnostics"] if enabled else [] + result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", *options, + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0 and output.read_bytes().startswith(b"MZ"), (result.stdout, result.stderr) + assert source.read_bytes() == original + assert diagnostics.read_bytes() == (b"" if enabled else original) + + def check_registry_inputs(relinker, work, windows): options = ["--windows"] if windows else [] for module_registry in (False, True): @@ -112,6 +178,7 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) + check_diagnostic_inputs(relinker, work) check_desktop_inputs(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 12accb979..4f2ba2837 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -35,7 +35,7 @@ relinker --macos source/input.elf eboot Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. -The executable, registry, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. +The executable, registry, Windows import diagnostics, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. ## Options From 70f03dc5e51ffaaa0e2e0c05d91a1354269e423e Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Mon, 12 Oct 2026 12:48:01 +0530 Subject: [PATCH 271/318] fix(relinker): write registries after the executable Co-Authored-By: Claude Opus 5.5 --- core/relinker/main.cpp | 15 ++++++------ .../relinker/tests/test_input_preservation.py | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 0e747853d..9048e3ec2 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -128,18 +128,13 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); } + std::vector registryPaths; if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); - std::vector registryPaths{ - outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json") - }; + registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); - const auto registryWriter = std::make_shared(); - fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); - for (std::size_t index = 0; index < guestArtifacts.size(); ++index) - fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); } auto byteWriter = std::make_shared(); @@ -174,6 +169,12 @@ int Run(const int argc, char* argv[]) { std::cout << "Guest module: " << Io::Utf8Path(artifact.Path) << '\n'; } fileWriter.Write(absPath, executableBytes); + if (args.writeRegistry) { + const auto registryWriter = std::make_shared(); + fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); + for (std::size_t index = 0; index < guestArtifacts.size(); ++index) + fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); + } if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n'; std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; std::cout << "Expected runtime layout (relative to the output executable):\n" diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 2114abbe3..925989308 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -53,6 +53,29 @@ def check_registry_inputs(relinker, work, windows): assert not (case / "app0").exists() +def check_registry_after_failed_patch(relinker, work): + case = work / "registry-failed-patch" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / "sample.prx").write_bytes(guest_fixture(PLAIN_SITE)) + (case / "sce_sys").mkdir() + (case / "sce_sys" / "icon0.png").write_bytes(b"\x89PNG\r\n\x1a\n" + bytes(40)) + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json"] + previous_registry = b"previous registry" + for registry in registries: + registry.write_bytes(previous_registry) + result = subprocess.run([str(relinker), "--windows", "--registry", str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Invalid Windows icon PNG" in result.stderr, (result.stdout, result.stderr) + assert output.read_bytes() == previous_output + assert all(registry.read_bytes() == previous_registry for registry in registries) + assert not (case / "app0").exists() + + def check_desktop_inputs(relinker, work): for desktop in (False, True): for bundled in (False, True): @@ -113,6 +136,7 @@ def main(): with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) check_desktop_inputs(relinker, work) + check_registry_after_failed_patch(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] check_registry_inputs(relinker, work, windows) From 656c81ac8bef307341bf51fc82d87de424e4a034 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX100@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:29:46 +0200 Subject: [PATCH 272/318] docs: clarify runtime code generation limits for --to-intel (#4257) --- docs/user/USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 12accb979..24b15482b 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -33,7 +33,7 @@ macOS output (x86-64 Mach-O, which runs under Rosetta on Apple silicon): relinker --macos source/input.elf eboot ``` -Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. +Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. `--to-intel` rewrites AMD-specific instructions found in the executable and bundled modules; it cannot translate AMD-specific instructions generated by a title at runtime, so such titles require an AMD host. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. The executable, registry, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. From 3048da7c0462bf85c0fb2452454b76690bfd750a Mon Sep 17 00:00:00 2001 From: Rain <204015000+tezzgod74@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:29:53 -0500 Subject: [PATCH 273/318] docs(compatibility): record DOOM homebrew on Windows with AMD RDNA 4 and RDNA 2 (#4312) Co-authored-by: Claude Opus 5.5 --- docs/user/COMPATIBILITY.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/user/COMPATIBILITY.md b/docs/user/COMPATIBILITY.md index 27dcd13c3..ee2e485fd 100644 --- a/docs/user/COMPATIBILITY.md +++ b/docs/user/COMPATIBILITY.md @@ -12,6 +12,7 @@ DOOM Shareware is a public homebrew, [lowbit/ps5-doom](https://github.com/lowbit - Linux, Ubuntu 24.04 container, Mesa llvmpipe, no hardware GPU (v1.1.1, `main` `0e173a46` plus [#1278](https://github.com/boykopovar/AnyPS5/pull/1278), [#1279](https://github.com/boykopovar/AnyPS5/pull/1279) and [#1307](https://github.com/boykopovar/AnyPS5/pull/1307), relinked with `--exclude-sce-module libc.prx`, see [#1497](https://github.com/boykopovar/AnyPS5/issues/1497)): E1M1, 35 FPS in game; audio and extended gameplay not tested. The `/download0` directory must exist beside the executable. - Linux, CachyOS, NVIDIA RTX 3080 Ti, driver 610.57.04, KDE Plasma Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 at 35 FPS in game; new game from the menu, movement, turning, firing, an item pickup and the automap checked with controller input from `APS5_PAD_INPUT_FILE`. Audio output opened at 48000 Hz but was not listened to; later maps and save/load not tested. The `/download0` directory did not need to exist before the first run. - Linux, CachyOS, AMD Ryzen 5 PRO 5650U integrated graphics (RADV RENOIR), Mesa 26.2.4, kernel 7.2.9, KDE Plasma 6.7.5 Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 and E1M2 completed at 35 FPS in game with a DualShock 4 through SDL (both sticks, D-pad, touchpad, options menu), audio heard, save/load round trip in `/download0`. The first run without a shader cache stayed on AGC compute presentation. `/dev/udmabuf` was accessible to the user, so shared direct memory was imported as dma-buf. +- Windows 11 Pro 10.0.26300, AMD Radeon RX 9060 XT (RDNA 4, driver 32.0.31041.1004) and the Ryzen 9 7950X integrated graphics (RDNA 2, driver 32.0.21045.5002), selected with `ANYPS5_GPU` (v1.1.1, `main` `b42ef24e0` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from their SELF and fSELF containers before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): on both devices, menus, a new game, movement, item pickups and firing in E1M1 with an Xbox One controller through SDL, audio heard. The first run without a shader cache was choppy and later runs were smooth; the frame rate was not measured. Later maps and save/load not tested. The `/download0` directory was created before the first run. - On the first run without a shader cache, the presentation kernel is compiled at its first dispatch, frame 1 does not complete in time and the title switches to the CPU scaler. Later runs with the cached kernel stay on AGC compute presentation (measured on Windows 11, RTX 3080, `main` `d70b8998`). -Not verified: Windows on current `main` without extra PRs, Linux with Intel GPUs. +Not verified: Linux with Intel GPUs. From 44e9050a58d7fe1aa69065aaee1471649a22e880 Mon Sep 17 00:00:00 2001 From: Lorky Date: Mon, 12 Oct 2026 09:30:54 +0200 Subject: [PATCH 274/318] fix(import-audit): parse export aliases as C++ tokens (#4196) Co-authored-by: lorkydey --- tools/import_audit.py | 3 +-- tools/nid_names.py | 5 ++-- tools/tests/test_import_audit.py | 42 ++++++++++++++++++++++++++++++++ tools/tests/test_nid_names.py | 5 ++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index a6c0f6b0d..276ce39f8 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -14,7 +14,6 @@ import progress ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" NID = re.compile(r"[A-Za-z0-9+\-]{11}") -EXPORT_ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w+)\)') NID_POSTFIX = "_nid_postfix" CLASSES = ("implemented", "stub", "absent", "module") PE_DIRECTORY_OFFSET = {0x20b: 112, 0x10b: 96} @@ -208,7 +207,7 @@ def source_stubs(source): for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} for file in library.rglob("*.cpp"): - for nid, name in EXPORT_ALIAS.findall(file.read_text(errors="ignore")): + for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): aliases[name] = nid group = progress.scan_library(library) done = {nid_of(name, aliases) for name in group["done_names"]} diff --git a/tools/nid_names.py b/tools/nid_names.py index 8dd9382e7..a633f94dd 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -81,7 +81,7 @@ def source_aliases(text): continue if re.fullmatch(r'"[A-Za-z0-9+\-]{11}"', literal) is None: continue - if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) and "unknown" in name.lower(): + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name): yield literal[1:-1], name @@ -97,7 +97,8 @@ def collect_unknowns(): for path in source_files(PRX): text = path.read_text(encoding="utf-8") for nid, name in source_aliases(text): - found.setdefault(nid, "%s:%s" % (path.relative_to(ROOT).as_posix(), name)) + if "unknown" in name.lower(): + found.setdefault(nid, "%s:%s" % (path.relative_to(ROOT).as_posix(), name)) return found diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 98084bf52..5534297e0 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -191,6 +191,48 @@ class ImportAuditTests(unittest.TestCase): records, _ = import_audit.audit([(nid, "libB.prx")], exports, stubs, {"libA.prx", "libB.prx"}, set()) self.assertEqual(records[0]["class"], "implemented") + def test_formatted_export_aliases_remain_stubs(self): + work = self.work + nid = "AAAAAAAAAAA" + (work.libs / "libSceAlias.prx").write_bytes(exporting(nid)) + registry = work.root / "game.json" + result = work.root / "audit.json" + write_registry(registry, [(nid, "libSceAlias.prx")]) + aliases = [ + 'APS5_EXPORT (\n "AAAAAAAAAAA",\n NAME\n);\n', + 'APS5_EXPORT(/* nid */ "AAAAAAAAAAA", /* name */ NAME);\n', + 'APS5_EX\\\nPORT("AAAAAAAAAAA", NAME);\n', + ] + for name in ("sceAliasUnknown", "sceAliasedCall"): + for alias in aliases: + with self.subTest(name=name, alias=alias): + write_source(work.source, "libSceAlias", alias.replace("NAME", name) + STUB_SOURCE.format(name=name)) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + record, = json.loads(result.read_text())["imports"] + self.assertEqual((record["class"], record["name"]), ("stub", name)) + + def test_comments_and_literals_do_not_override_export_aliases(self): + work = self.work + nid, name = "AAAAAAAAAAA", "sceAliasUnknown" + (work.libs / "libSceAlias.prx").write_bytes(exporting(nid)) + registry = work.root / "game.json" + result = work.root / "audit.json" + write_registry(registry, [(nid, "libSceAlias.prx")]) + source = 'APS5_EXPORT("AAAAAAAAAAA", sceAliasUnknown);\n' + STUB_SOURCE.format(name=name) + decoys = [ + '// APS5_EXPORT("BBBBBBBBBBB", sceAliasUnknown);\n', + '/* APS5_EXPORT("BBBBBBBBBBB", sceAliasUnknown); */\n', + 'auto example = R"text(APS5_EXPORT("BBBBBBBBBBB", sceAliasUnknown);)text";\n', + ] + for decoy in decoys: + with self.subTest(decoy=decoy): + write_source(work.source, "libSceAlias", source + decoy) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + record, = json.loads(result.read_text())["imports"] + self.assertEqual((record["class"], record["name"]), ("stub", name)) + @unittest.skipUnless((import_audit.ROOT / ".git").exists(), "needs a git checkout") def test_json_is_stamped_with_the_source_commit(self): work = self.work diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 0ac509da5..66b18f5df 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -161,6 +161,11 @@ int APS5_VABI sceReal() { return 0; } self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n') self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) + def test_known_aliases_are_not_unknown_exports(self): + self.write_source('APS5_EXPORT("AAAAAAAAAAA", sceKnownCall);\n' + 'APS5_EXPORT("BBBBBBBBBBB", exampleUnknown00);\n') + self.assertEqual(nid_names.collect_unknowns(), {"BBBBBBBBBBB": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) + def test_windows_source_paths_use_portable_separators(self): self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\n') relative = PureWindowsPath("core/libs/prx/libSceExample/Export.cpp") From 0f0291e39b3f936d7c9214dba153fdea6034fefc Mon Sep 17 00:00:00 2001 From: Paritosh Lahre <155938586+SandstromPL@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:01:03 +0530 Subject: [PATCH 275/318] fix(tools): run pr_overlap on gh older than 2.48 (#4289) Co-authored-by: Claude Opus 5.5 --- tools/pr_overlap.py | 6 +++--- tools/tests/test_pr_overlap.py | 13 ++++++------- tools/tests/test_pr_overlap_dependencies.py | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 279006dba..183d01311 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -3,7 +3,7 @@ import json import os import re import subprocess -from itertools import chain, combinations +from itertools import combinations from pathlib import Path from urllib.parse import urlencode @@ -37,9 +37,9 @@ def depends_on(body): def open_prs(base): query = urlencode({"state": "open", "base": base, "per_page": 100}) - out = gh("api", "--paginate", "--slurp", f"repos/{REPO}/pulls?{query}") + out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") prs = {} - for pr in chain.from_iterable(json.loads(out)): + for pr in json.loads(out): prs[pr["number"]] = { "ref": f"refs/pr/{pr['number']}", "sha": pr["head"]["sha"], diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index f37570b78..31488bb0c 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -19,8 +19,7 @@ class OpenPullRequestsTests(unittest.TestCase): def response(*args): if args[0] == "api": - return json.dumps([pull_requests[start:start + 100] - for start in range(0, len(pull_requests), 100)]) + return json.dumps(pull_requests) limit = int(args[args.index("-L") + 1]) return json.dumps([ {"number": pr["number"], "author": pr["user"], "headRefOid": pr["head"]["sha"], @@ -38,18 +37,18 @@ class OpenPullRequestsTests(unittest.TestCase): def test_base_branch_is_encoded_in_the_paginated_request(self): with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ - patch.object(pr_overlap, "gh", return_value="[[]]") as gh: + patch.object(pr_overlap, "gh", return_value="[]") as gh: self.assertEqual(pr_overlap.open_prs("topic/one&two"), {}) gh.assert_called_once_with( - "api", "--paginate", "--slurp", + "api", "--paginate", "repos/owner/project/pulls?state=open&base=topic%2Fone%26two&per_page=100", ) def test_single_page_preserves_author_head_and_dependencies(self): - pages = [[{"number": 7, "user": {"login": "contributor"}, "head": {"sha": "head-sha"}, - "body": "Depends on: none\nUnrelated issue #42"}]] + pull_requests = [{"number": 7, "user": {"login": "contributor"}, "head": {"sha": "head-sha"}, + "body": "Depends on: none\nUnrelated issue #42"}] with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ - patch.object(pr_overlap, "gh", return_value=json.dumps(pages)): + patch.object(pr_overlap, "gh", return_value=json.dumps(pull_requests)): found = pr_overlap.open_prs("main") self.assertEqual(found, { 7: {"ref": "refs/pr/7", "sha": "head-sha", "author": "contributor", "depends": set()}, diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index f93bb828c..f7395491d 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -12,7 +12,7 @@ def pull_requests(body): response = [{"number": number, "user": {"login": "contributor"}, "head": {"sha": str(number) * 40}, "body": body if number == 1 else "Depends on: none"} for number in (1, 2)] with patch.object(pr_overlap, "REPO", "owner/repo", create=True), \ - patch.object(pr_overlap, "gh", return_value=json.dumps([response])): + patch.object(pr_overlap, "gh", return_value=json.dumps(response)): return pr_overlap.open_prs("main") From f961c52ad8c6e9dbdba87b97dac1cebdee53ee5d Mon Sep 17 00:00:00 2001 From: Bharat Yadav Date: Mon, 12 Oct 2026 13:01:13 +0530 Subject: [PATCH 276/318] fix(release): exclude removed libraries from packages (#4297) --- tools/package_release.py | 2 +- tools/tests/test_package_release.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/package_release.py b/tools/package_release.py index 476314e0e..52f0b3ffc 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -18,7 +18,7 @@ def package(platform, build, output, version): if missing: raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") executable = "relinker.exe" if platform == "windows" else "relinker" - files = list(libraries) + files = [library for library in libraries if library.name in expected] if platform == "windows": runtime = Path("C:/winlibs/mingw64/bin") files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 2724ef6e4..a969a1692 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -56,6 +56,18 @@ class ReleasePackageTests(unittest.TestCase): with zipfile.ZipFile(self.output / "prx-linux-v1.0.zip") as archive: self.assertEqual(archive.namelist(), ["libs/libc.prx"]) + def test_removed_libraries_are_not_packaged_from_a_reused_build(self): + stale = self.library.with_name("libRemoved.prx") + stale.write_bytes(b"obsolete library") + package_release.package("linux", self.build, self.output, "v1.0") + with zipfile.ZipFile(self.output / "prx-linux-v1.0.zip") as archive: + self.assertEqual(archive.namelist(), ["libs/libc.prx"]) + self.assertEqual(archive.read("libs/libc.prx"), b"library bytes") + with tarfile.open(self.output / "prx-linux-v1.0.tar.gz") as archive: + self.assertEqual(archive.getnames(), ["libs/libc.prx"]) + self.assertEqual(archive.extractfile("libs/libc.prx").read(), b"library bytes") + self.assertEqual(stale.read_bytes(), b"obsolete library") + def test_tar_and_zip_include_the_same_symlink_contents(self): target = self.library.with_name("libc.target") self.library.rename(target) From 436e310ddb56fcf5dd1d40ba1f8113fe8cb8c113 Mon Sep 17 00:00:00 2001 From: Paritosh Lahre <155938586+SandstromPL@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:01:24 +0530 Subject: [PATCH 277/318] fix(conventions): report 3rdparty submodules without a .gitmodules entry (#4304) Co-authored-by: Claude Opus 5.5 --- tools/check_conventions.py | 11 +++++++ .../tests/test_check_conventions_comments.py | 31 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 01a2b012b..92d5386bb 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -143,7 +143,18 @@ class Check: if match and not match.group(1).startswith("3rdparty/"): self.report("system-dependency", ".gitmodules", number, match.group(1)) + def submodules(self): + result = subprocess.run(["git", "config", "--no-includes", "-z", "--blob", f"{self.head}:.gitmodules", "--list"], + capture_output=True, encoding="utf-8", errors="replace") + values = {key: value for key, _, value in (entry.partition("\n") for entry in result.stdout.split("\0") if entry)} + return {path for key, path in values.items() if key.endswith(".path") and values.get(f"{key[:-5]}.url")} + def repository(self): + gitlinks = {path for path, mode in self.modes.items() if mode == "160000" and path.startswith("3rdparty/")} + checked = gitlinks if ".gitmodules" in self.status else gitlinks & self.status.keys() + if checked: + for path in sorted(checked - self.submodules()): + self.report("system-dependency", path, 0, "submodule without a url in .gitmodules") for path, (added, deleted) in self.numstat.items(): if path.startswith("3rdparty/"): continue diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 89df82af5..44616324e 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -295,6 +295,14 @@ class RepositoryChecksTests(unittest.TestCase): self.git("add", ".") self.git("commit", "-qm", "test: add convention fixture") + def add_thirdparty_submodule(self, gitmodules=None): + head = self.git("rev-parse", "HEAD") + if gitmodules is not None: + (self.root / ".gitmodules").write_text(gitmodules, encoding="utf-8") + self.git("add", ".gitmodules") + self.git("update-index", "--add", "--cacheinfo", "160000", head, "3rdparty/vendor") + self.git("commit", "-qm", "build: add submodule") + def test_quoted_utf8_paths_are_checked(self): name = "docs/dev/測試guide.md" if os.name == "nt" else "docs/dev/測試\tguide.md" path = self.root / name @@ -337,13 +345,30 @@ class RepositoryChecksTests(unittest.TestCase): self.assertIn(("system-dependency", "vendor", 0, "submodule outside 3rdparty/"), check.findings) def test_thirdparty_submodule_remains_allowed(self): - head = self.git("rev-parse", "HEAD") - self.git("update-index", "--add", "--cacheinfo", "160000", head, "3rdparty/vendor") - self.git("commit", "-qm", "build: add allowed submodule") + self.add_thirdparty_submodule('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n' + "\turl = https://example.com/vendor.git\n") check = Check("HEAD~1", "HEAD") check.repository() self.assertEqual(check.findings, []) + def test_thirdparty_submodule_without_gitmodules_entry_is_reported(self): + self.add_thirdparty_submodule() + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, + [("system-dependency", "3rdparty/vendor", 0, "submodule without a url in .gitmodules")]) + + def test_dropping_a_submodule_url_is_reported(self): + self.add_thirdparty_submodule('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n' + "\turl = https://example.com/vendor.git\n") + (self.root / ".gitmodules").write_text('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n', encoding="utf-8") + self.git("add", ".gitmodules") + self.git("commit", "-qm", "build: drop submodule url") + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, + [("system-dependency", "3rdparty/vendor", 0, "submodule without a url in .gitmodules")]) + def test_relative_link_url_decoding(self): self.assertEqual(links("docs/dev/index.md", "[guide](./space%20name.md#details)"), [("./space%20name.md", "docs/dev/space name.md")]) From a3cba2d1ff99d83bffe7072c36e2bbd16f9342b8 Mon Sep 17 00:00:00 2001 From: Tyler Westerberg Date: Sat, 10 Oct 2026 20:53:13 -0400 Subject: [PATCH 278/318] fix(agc-driver): move indices far past every vertex fetch's records to the first zero vertex Goat Simulator 3 draws a triangle list whose indices are all 0xffffffff with primitive restart off. Each fetch past a descriptor's records reads zeros (out-of-bounds select 0 or 1), but the zero-padded vertex copies were sized to the highest index: 160 GiB, which vkAllocateMemory refused. When the highest index is at least 64Ki vertices past the largest per-vertex record count, the draw now uses a copy of its indices with every index past that count moved to it. Each attribute reads zeros there as at the original index; only the vertex index the shader sees changes, so indices closer to the records are left as they are. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/VertexInput.hpp | 39 +++++++++++++++++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 18 +++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 20 ++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 78 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 0bc1a7c21..c5de807ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -227,6 +227,45 @@ inline VertexCopyPlan PlanVertexCopies(std::span fetches) { return plan; } +inline std::optional ZeroFetchVertex(std::span attributes) { + std::uint32_t zeroFrom = 0; + for (const auto& attribute : attributes) { + const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; + if (NullVertexDescriptor(attribute) || attribute.fetchIndex != 0 || stride == 0) continue; + if (VertexBufferOutOfBoundsSelect(attribute) > 1u) return std::nullopt; + zeroFrom = std::max(zeroFrom, attribute.resource.fields[2]); + } + return zeroFrom; +} + +inline std::optional ClampDrawIndices(std::span indices, std::uint32_t indexSize, bool skipRestart, std::uint32_t limit) { + Require(indexSize == 2 || indexSize == 4, "unsupported index size"); + const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; + std::optional highest; + for (std::size_t offset = 0; offset + indexSize <= indices.size(); offset += indexSize) { + std::uint32_t index = 0; + if (indexSize == 2) { + std::uint16_t value = 0; + std::memcpy(&value, indices.data() + offset, sizeof(value)); + index = value; + } else { + std::memcpy(&index, indices.data() + offset, sizeof(index)); + } + if (skipRestart && index == restartIndex) continue; + if (index > limit) { + index = limit; + if (indexSize == 2) { + const auto value = static_cast(index); + std::memcpy(indices.data() + offset, &value, sizeof(value)); + } else { + std::memcpy(indices.data() + offset, &index, sizeof(index)); + } + } + highest = std::max(highest.value_or(0u), index); + } + return highest; +} + inline std::optional HighestDrawIndex(std::span indices, std::uint32_t indexSize, bool skipRestart) { Require(indexSize == 2 || indexSize == 4, "unsupported index size"); const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 2f3bf711d..93cb7a68d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1115,6 +1115,24 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P inputs.nothing = true; return inputs; } + constexpr std::uint32_t ZeroFetchSlack = 0x10000u; + if (const auto zeroFrom = state.stages.mesh ? std::nullopt : ZeroFetchVertex(shaders.front().program->vertexAttributes)) { + const auto limit = *zeroFrom > draw.firstVertex ? *zeroFrom - draw.firstVertex : 0u; + if (*highest >= limit && *highest - limit >= ZeroFetchSlack) { + GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast(indexBytes)); + auto clamped = std::make_shared(context, static_cast(indexBytes), VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + const auto bytes = clamped->Bytes().first(static_cast(indexBytes)); + GuestMemory::Read(draw.indexAddress, bytes, draw.indexSize); + highest = ClampDrawIndices(bytes, draw.indexSize, skipRestart, limit); + import = nullptr; + copy = DrawInputCopy{}; + copy.buffer = std::move(clamped); + if (!highest) { + inputs.nothing = true; + return inputs; + } + } + } Require(*highest <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit"); Require(!fanGeometry || !state.primitiveRestart || *highest != (draw.indexSize == 2 ? 0xffffu : 0xffffffffu), "primitive restart in a triangle fan geometry draw is unsupported"); inputs.maxIndex = *highest; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 39c8dd8b2..bb890c28d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4215,6 +4215,26 @@ void validationTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch"); + { + auto wider = attribute; + wider.resource.fields[2] = 9; + auto instanced = attribute; + instanced.fetchIndex = 1; + instanced.resource.fields[2] = 1000; + auto constant = attribute; + constant.resource.fields[1] = 0; + const std::array attributes{attribute, wider, instanced, constant}; + Require(AgcDriver::Graphics::ZeroFetchVertex(attributes) == 9u, "the first vertex reading zeros is not past the largest per-vertex record count"); + auto clamped = attribute; + clamped.resource.fields[3] |= 2u << 28u; + const std::array selective{attribute, clamped}; + Require(!AgcDriver::Graphics::ZeroFetchVertex(selective).has_value(), "an attribute with out-of-bounds select 2 was taken to read zeros past its records"); + std::array indices{0xffffffffu, 2u, 0x12345u, 0xffffffffu}; + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, true, 9u) == 9u && indices == std::array{0xffffffffu, 2u, 9u, 0xffffffffu}, "clamping kept an index past the limit or moved a restart index"); + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, false, 9u) == 9u && indices == std::array{9u, 2u, 9u, 9u}, "clamping without restart kept a 0xffffffff index"); + std::array narrow{0xffffu, 1u}; + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed"); + } attribute.fetchIndex = 1; Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4481d2cbc..297e683f9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -61,6 +61,7 @@ Throughout the project, every function at every stage either **does exactly what - [kern.sdk_version](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - PPSA01530, PPSA23827 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), and [scePadUnknown_n3kSX62fgNo](../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. - [sceVoiceQoSSetConnectionAttribute](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) +- [Indices far past every vertex fetch](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. - [sceHttp2WebSocketSetPingInterval](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. - [sceKernelGetProsperoSystemSwVersion](../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it From 083c98a0672ed34b06766ee853c0428ef68d85ff Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 12 Oct 2026 07:49:09 +0000 Subject: [PATCH 279/318] fix(libkernel): leave a once initializer's own cleanup handlers alone when it throws pthread_once and scePthreadOnce removed their reset handler with a pop that runs whatever handler is on top of the cleanup stack. An initializer that pushed a handler of its own and then threw left that handler on top, so the pop ran it with dangling state. Both now record the stack top before pushing the reset handler; on a throw they restore the stack to it and reset the control directly, and a normal return restores it as well. Reported in review by Hussein Kadhim, who fixed it the same way. Co-authored-by: Hussein Kadhim Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/Posix/Once.cpp | 6 ++++-- .../libs/prx/libkernel/Pthread/include/Cancel.hpp | 2 ++ core/libs/prx/libkernel/Pthread/src/Thread.cpp | 14 ++++++++++++-- core/libs/tests/GuestOnce.cpp | 15 +++++++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp index dc09e2cb4..6919e008b 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp @@ -37,14 +37,16 @@ extern "C" int APS5_VABI pthread_once_nid_postfix( // Initializers may initialize other once controls. Never run guest code // under the shared lock. The lock publishes all initializer writes. CleanupHandler reset{}; + void* const mark = ThreadCancel::CleanupMark(); __pthread_cleanup_push_imp_nid_postfix(ResetOnce, control, &reset); try { initialize(); } catch (...) { - __pthread_cleanup_pop_imp_nid_postfix(1); + ThreadCancel::CleanupRestore(mark); + ResetOnce(control); throw; } - __pthread_cleanup_pop_imp_nid_postfix(0); + ThreadCancel::CleanupRestore(mark); lock.lock(); control->state = 1; lock.unlock(); diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index b85a2a375..6a3dad661 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -28,6 +28,8 @@ namespace ThreadCancel { bool Requested(); void Check(); +void* CleanupMark(); +void CleanupRestore(void* mark); void BeginWait(TimedWait::Condition* condition); void EndWait(); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index cc471f5f5..72cc0ff3e 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -353,6 +353,14 @@ void ThreadCancel::Check() { if (Requested()) scePthreadExit(GUEST_CANCELED); } +void* ThreadCancel::CleanupMark() { + return CurrentGuestState().cleanupHandlers; +} + +void ThreadCancel::CleanupRestore(void* mark) { + CurrentGuestState().cleanupHandlers = static_cast(mark); +} + void ThreadCancel::BeginWait(TimedWait::Condition* condition) { if (!CurrentGuestState().current) return; std::lock_guard lock(CurrentGuestState().current->cancelLock); @@ -814,14 +822,16 @@ int APS5_VABI scePthreadOnce(int32_t* once, void (APS5_VABI* init)(void)) { state.wait(Running, std::memory_order_acquire); } CleanupHandler reset{}; + void* const mark = ThreadCancel::CleanupMark(); __pthread_cleanup_push_imp_nid_postfix(ResetOnce, once, &reset); try { init(); } catch (...) { - __pthread_cleanup_pop_imp_nid_postfix(1); + ThreadCancel::CleanupRestore(mark); + ResetOnce(once); throw; } - __pthread_cleanup_pop_imp_nid_postfix(0); + ThreadCancel::CleanupRestore(mark); state.store(Done, std::memory_order_release); state.notify_all(); return SCE_OK; diff --git a/core/libs/tests/GuestOnce.cpp b/core/libs/tests/GuestOnce.cpp index ea35859ab..3e60f58ec 100644 --- a/core/libs/tests/GuestOnce.cpp +++ b/core/libs/tests/GuestOnce.cpp @@ -18,6 +18,7 @@ void APS5_VABI scePthreadExit(void* retval); int APS5_VABI scePthreadCancel(Pthread thread); void APS5_VABI scePthreadTestcancel(); Pthread APS5_VABI scePthreadSelf(); +void APS5_VABI __pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info); } static void Require(bool value) { if (!value) std::abort(); } static GuestOnce control{}, nested{}, retry{}; @@ -42,6 +43,14 @@ static void APS5_VABI CancelInsideInitializer() { scePthreadTestcancel(); } static void APS5_VABI CountRetry() { ++retried; } +static void APS5_VABI AbortCleanup(void*) { std::abort(); } +static void APS5_VABI PushAndThrow() { + std::uintptr_t info[8]; + __pthread_cleanup_push_imp_nid_postfix(AbortCleanup, nullptr, info); + throw 7; +} +static GuestOnce pushed{}; +static std::int32_t scePushed = 0; struct OnceCall { GuestOnce* control; std::int32_t* sceControl; Initializer initialize; }; static void* APS5_VABI CallOnce(void* argument) { const auto& call = *static_cast(argument); @@ -83,4 +92,10 @@ int main() { LeaveInsideInitializer({nullptr, &sceCancelled, CancelInsideInitializer}); Require(sceCancelled == 0); Require(scePthreadOnce(&sceCancelled, CountRetry) == 0 && sceCancelled == 1 && retried == 4); + try { pthread_once_nid_postfix(&pushed, PushAndThrow); std::abort(); } + catch (int value) { Require(value == 7 && pushed.state == 0); } + Require(pthread_once_nid_postfix(&pushed, CountRetry) == 0 && pushed.state == 1 && retried == 5); + try { scePthreadOnce(&scePushed, PushAndThrow); std::abort(); } + catch (int value) { Require(value == 7 && scePushed == 0); } + Require(scePthreadOnce(&scePushed, CountRetry) == 0 && scePushed == 1 && retried == 6); } From ddb66806678a635b59cad76954dc277d0bd65457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 10 Oct 2026 10:18:14 +0200 Subject: [PATCH 280/318] perf(recompiler): read buffer V# words in the shader when modules are built at registration Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Execution/src/VulkanDevice.cpp | 1 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 14 ++- .../execution/BufferDescriptorRuntime.cpp | 113 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../Optimization/DescriptorBindingBuilder.hpp | 2 +- .../src/DescriptorBindingBuilder.cpp | 14 ++- core/shader/recompiler/Recompiler.cpp | 7 +- core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 2 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 42 ++++--- 14 files changed, 173 insertions(+), 34 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6c94ffc4b..d7f9bef59 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1010,6 +1010,8 @@ target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestSh target_include_directories(agc_driver_prepared_exports_tests PRIVATE $) +target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $) + target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index d24dbbc56..858e855dc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2926,6 +2926,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.narrowSubgroupClock = state->narrowSubgroupClock; target.float64DenormPreserve = state->float64DenormPreserve; target.fixedPushSlots = state->graphicsPipelineLibrary; + target.registrationModules = state->graphicsPipelineLibrary; target.srgbDecodeFormats = state->srgbDecodeFormats; if (state->meshShader) { const auto& mesh = state->meshLimits; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1526845de..e191f6d1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1625,12 +1625,12 @@ void verifyPixelRequestSerialization() { minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRAAAAA=", "new requests did not use serialization version 16"); + require(requestPrefix(encoded, 8u) == "NVNQQREAAAA=", "new requests did not use serialization version 17"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; for (std::size_t bytes = 0; bytes < 18u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source or alpha-to-coverage flag was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQREAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRIAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } } @@ -1725,7 +1725,7 @@ void verifyLegacyPixelRequests() { require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader"); require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned"); require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned"); - require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve, "legacy request trailer was misread"); + require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules, "legacy request trailer was misread"); const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping"); } @@ -3218,6 +3218,14 @@ int main(int argc, char** argv) { auto float64Denormals = uncached; float64Denormals.target.float64DenormPreserve = true; require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float64Denormals)).request.target.float64DenormPreserve, "f64 denormal preservation was lost in serialization"); + auto registrationModules = uncached; + registrationModules.target.registrationModules = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(registrationModules)).request.target.registrationModules, "registration modules were lost in serialization"); + std::vector plainKey; + std::vector registrationKey; + RecompileCacheKey::Build(uncached, plainKey); + RecompileCacheKey::Build(registrationModules, registrationKey); + require(plainKey != registrationKey, "registration modules did not change the cache key"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp new file mode 100644 index 000000000..1948f7e20 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -0,0 +1,113 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t InputBytes = 256; +constexpr std::uint32_t Fill = 0xdeadbeefu; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020082, 0xe0301000, 0x80000201, 0xbf8c3f70, 0xe0701000, 0x80010201, 0xbf810000, +}; + +struct Case { + const char* name; + std::uint32_t baseOffset; + std::uint32_t records; + bool present; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +ShaderRecompiler::RecompileRequest Request(std::span userData, const ShaderRecompiler::SpirvTarget& target, std::span memory) { + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + return { + {ShaderStage::Compute, reinterpret_cast(Code.data()), Code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; +} + +void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { + const std::string mode = registrationModules ? "registration modules" : "specialized modules"; + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(std::span(Code))}}}; + std::array userData{}; + auto target = device.ComputeTarget(Lanes); + target.registrationModules = registrationModules; + const auto request = Request(userData, target, memory); + const auto handle = ShaderRecompiler::PrepareShader(request); + const std::array cases{{ + {"aligned", 0, 128, true}, + {"base +1", 1, 128, true}, + {"base +2", 2, 128, true}, + {"base +3", 3, 128, true}, + {"records 37", 0, 37, true}, + {"records 64 base +2", 2, 64, true}, + {"absent", 0, 128, false}, + }}; + const void* first = nullptr; + for (const auto& test : cases) { + Output.fill(Fill); + const auto input = test.present ? BufferDescriptor(Input.data() + test.baseOffset, test.records) : std::array{0u, 0u, 0u, 0x31016facu}; + const auto output = BufferDescriptor(Output.data(), sizeof(Output)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + ShaderRecompiler::SrtRuntime runtime{}; + runtime.userData = userData; + const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); + const auto result = ShaderRecompiler::MaterializeShader(request, *capture, *handle); + if (first == nullptr) first = result->spirv.data(); + if (registrationModules) Require(result->spirv.data() == first, "buffer descriptor runtime, " + mode + ", " + test.name + ": the V# specialized the module"); + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + std::uint32_t expected = 0; + if (test.present && lane * 4u + 4u <= test.records) std::memcpy(&expected, Input.data() + test.baseOffset + lane * 4u, sizeof(expected)); + Require(Output[lane] == expected, "buffer descriptor runtime, " + mode + ", " + test.name + ": lane " + std::to_string(lane) + " is " + Hex(Output[lane]) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < InputBytes; ++index) Input[index] = static_cast(index * 41u + 3u + (index >> 3u)); + Run(*device, false); + Run(*device, true); + std::puts("buffer descriptor runtime tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index be68766aa..b50278685 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -240,6 +240,7 @@ private: append(key, value.srgbDecodeFormats); append(key, value.narrowSubgroupClock); append(key, value.float64DenormPreserve); + append(key, value.registrationModules); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 5a76e67bb..b2f5d2007 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -734,7 +734,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(16u); + writer.WriteU32(17u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -754,6 +754,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const if (request.context.compute.has_value()) writer.WriteU32(request.context.compute->scratchDwords); writer.WriteBool(request.target.narrowSubgroupClock); writer.WriteBool(request.target.float64DenormPreserve); + writer.WriteBool(request.target.registrationModules); return base64Encode(buffer); } @@ -762,7 +763,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 16u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 17u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -783,6 +784,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 10u && result.request.context.compute.has_value()) result.request.context.compute->scratchDwords = reader.ReadU32(); if (version >= 12u) result.request.target.narrowSubgroupClock = reader.ReadBool(); if (version >= 16u) result.request.target.float64DenormPreserve = reader.ReadBool(); + if (version >= 17u) result.request.target.registrationModules = reader.ReadBool(); return result; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 01ece57e4..224a452d9 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -33,7 +33,7 @@ struct DescriptorBindingPlan { class DescriptorBindingBuilder { public: - DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}) const; + DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false) const; DescriptorBindingPlan Select(const DescriptorBindingPlan& plan, std::span bindings) const; void Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads, std::span exportMappings = {}) const; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 6b68b292a..32505b852 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -250,7 +250,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con allocation.specialization = std::move(plan.specialization); } -DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings) const { +DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules) const { if (stage == IrShaderStage::Pixel && exportMappings.size() != 8u) fail("fragment runtime export mappings are missing"); if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; @@ -280,11 +280,13 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l auto alignment = static_cast(buffer.typedAlignment); if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast(format)).byteSize, 4u)); if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element"); - const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; - plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); - plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); - plan.specialization.push_back({first + 2u, present ? 1u : 0u}); - plan.specialization.push_back({first + 3u, misalignment}); + if (!registrationModules) { + const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; + plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); + plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); + plan.specialization.push_back({first + 2u, present ? 1u : 0u}); + plan.specialization.push_back({first + 3u, misalignment}); + } if (buffer.formattedReadMask != 0u) { for (std::uint32_t component = 0; component < 4u; ++component) { if ((buffer.formattedReadMask & (1u << component)) == 0u) continue; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 4b9660270..5fda2ba0b 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -418,6 +418,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets; targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock; targetOptions.float64DenormPreserve = request.target.float64DenormPreserve; + targetOptions.registrationModules = request.target.registrationModules; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; @@ -601,7 +602,7 @@ struct PreparedBindingPlan { std::map, std::shared_ptr> modules; }; -std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports) { +std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool registrationModules) { struct BindingPlanKeyStorage {}; auto& key = HostThreadLocal, BindingPlanKeyStorage>(); key.clear(); @@ -642,7 +643,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } std::call_once(plan->ready, [&] { try { - plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports); + plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, registrationModules); } catch (...) { plan->failure = std::current_exception(); } @@ -704,7 +705,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .barycentricEmulation = artifact.barycentricEmulation, .variantId = artifact.variantId }; - const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}); + const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules); const DescriptorBindingPlan* bindingPlan = &plan->bindings; std::shared_ptr entry; struct LocalModuleKeyStorage {}; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 85f9689e8..731ffa1c7 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -229,6 +229,7 @@ struct SpirvTarget { bool narrowSubgroupClock = false; bool fixedPushSlots = false; bool float64DenormPreserve = false; + bool registrationModules = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 0d18fb6e5..dbd24615a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -21,6 +21,7 @@ struct SpirvTargetOptions { bool nonConstantImageOffsets = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool registrationModules = false; }; class SpirvEmitter { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index ca69aa93c..ff712c17e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -91,6 +91,8 @@ struct SpirvEmitterState { bool splitSubgroup = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool registrationModules = false; + std::uint32_t bufferMisalignment = 0; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; // The target's SPIR-V version and what the device accepts, for capabilities an emitter adds diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 8a0951153..df64b366a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -250,6 +250,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.nonConstantImageOffsets = target.nonConstantImageOffsets; state.narrowSubgroupClock = target.narrowSubgroupClock; state.float64DenormPreserve = target.float64DenormPreserve; + state.registrationModules = target.registrationModules; state.hostSubgroupSize = target.subgroupSize; state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u; if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 2e6a1b0db..0a3a991da 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -57,9 +57,10 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const auto* handle = instruction.Argument(0)->Resolve(); if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) context.Fail(instruction, "requires a runtime four-dword V#"); const auto& memory = context.Memory(instruction); + const bool runtime = memory.gpuDescriptor || state.registrationModules; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - const auto word1 = memory.gpuDescriptor ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); - const auto word3 = memory.gpuDescriptor ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + const auto word3 = runtime ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); const auto u32 = TypeU32(state); auto base = ConstantU32(state, 0u); if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) { @@ -82,7 +83,8 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst } if (memory.gpuDescriptor) faultIf(context, instruction, invalid, base, 0u, BdaAbi::FaultReason::InvalidDescriptor); const auto stride = field(state, word1, 16u, 14u); - const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + const auto present = runtime ? both(state, nonzero(state, context.Arg(*handle, 2u)), Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, context.Arg(*handle, 0u)), nonzero(state, field(state, word1, 0u, 16u)))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : present; return {base, context.Arg(*handle, 2u), stride, word3, both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))), index, valid}; } @@ -140,8 +142,10 @@ MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& std::uint32_t baseMisalignment(SpirvValueEmitContext& context, const IrValue& instruction) { const auto& memory = context.Memory(instruction); if (memory.kind == ResourceKind::ScalarBuffer) return ConstantU32(context.state, 0u); - const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - return context.state.module.SpecializationConstant(TypeU32(context.state), first + 3u, 0u); + auto& state = context.state; + if (state.bufferMisalignment != 0u) return state.bufferMisalignment; + if (state.registrationModules) return Binary(state, spv::OpBitwiseAnd, TypeU32(state), context.Arg(*instruction.Argument(0)->Resolve(), 0u), ConstantU32(state, 3u)); + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords + 3u, 0u); } std::uint32_t directWordIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t address) { @@ -401,7 +405,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr auto function = state.formattedBufferFunctions.find(key); if (function == state.formattedBufferFunctions.end()) { const auto id = state.module.AllocateId(); - const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32); + const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32); const auto label = state.currentLabel; const auto byteOffset = state.memoryByteOffsets.at(element); struct Restore { @@ -412,27 +416,23 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr ~Restore() { state.module.EndHelperFunction(); state.memoryByteOffsets.at(element) = byteOffset; + state.bufferMisalignment = 0u; state.currentLabel = label; } }; state.module.BeginHelperFunction(); const Restore restore{state, element, label, byteOffset}; state.module.AddFunction(spv::OpFunction, resultType, id, spv::FunctionControlDontInlineMask, functionType); - std::array parameters{}; - for (std::uint32_t index = 0; index < (store ? 6u : 5u); ++index) { + std::array parameters{}; + for (std::uint32_t index = 0; index < (store ? 11u : 10u); ++index) { parameters[index] = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionParameter, index == 5u ? valueType : u32, parameters[index]); + state.module.AddFunction(spv::OpFunctionParameter, index == 10u ? valueType : u32, parameters[index]); } EmitLabel(state, state.module.AllocateId()); state.memoryByteOffsets.at(element) = parameters[4]; - const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - const auto word1 = state.module.SpecializationConstant(u32, first, 0u); - const auto word3 = state.module.SpecializationConstant(u32, first + 1u, 0u); - const auto stride = field(state, word1, 16u, 14u); - const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))); - const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); - const RuntimeDescriptor shared{ConstantU32(state, 0u), parameters[3], stride, word3, swizzle, parameters[0], valid, parameters[1], parameters[2]}; - const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[5] : 0u); + state.bufferMisalignment = parameters[9]; + const RuntimeDescriptor shared{ConstantU32(state, 0u), parameters[3], parameters[5], parameters[6], nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; + const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[10] : 0u); if (store) state.module.AddFunction(spv::OpReturn); else state.module.AddFunction(spv::OpReturnValue, value); state.module.AddFunction(spv::OpFunctionEnd); @@ -440,8 +440,12 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr } const auto offset = Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), ConstantU32(state, memory.offset)); const auto result = state.module.AllocateId(); - if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), data); - else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element)); + const auto flag = [&](std::uint32_t condition) { return Select(state, u32, condition, ConstantU32(state, 1u), ConstantU32(state, 0u)); }; + const auto swizzle = flag(resource.swizzle); + const auto valid = flag(resource.valid); + const auto misalignment = baseMisalignment(context, instruction); + if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, misalignment, data); + else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, misalignment); return store ? ConstantU32(state, 0u) : result; } From 765ea6a10ffeb21bd6e350aa71b9466e9b580f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 13:35:13 +0200 Subject: [PATCH 281/318] perf(recompiler): keep descriptor heaps uncompacted and read image modes at run time when modules are built at registration Co-Authored-By: Claude --- .../tests/execution/EmulatedColorCompare.cpp | 2 + .../tests/execution/TypedHeaps.cpp | 3 +- core/shader/recompiler/Deserialize.cpp | 2 +- .../include/Optimization/BindingAllocator.hpp | 2 +- .../Optimization/DescriptorBindingBuilder.hpp | 1 + .../Optimization/src/BindingAllocator.cpp | 4 +- .../src/DescriptorBindingBuilder.cpp | 38 ++++++++++++++----- core/shader/recompiler/Recompiler.cpp | 2 +- .../SpirvMemory/SpirvDescriptors.hpp | 1 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 17 ++------- .../src/SpirvMemory/SpirvDescriptors.cpp | 21 +++++++++- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- 12 files changed, 64 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index cc3452070..3347835d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -243,6 +243,7 @@ std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const const auto result = Compile(device, Format8888UNorm, sampler, code, useCache); if (expectedVariant != 0) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); for (const auto& binding : result.bindings) { + if (device.Target().registrationModules) break; Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "emulated comparison retained an unused sampler association"); } @@ -293,6 +294,7 @@ void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char Output.fill(-1.0f); const auto result = Compile(device, Format8888UNorm, sampler, CubeCode, false, true, true); for (const auto& binding : result.bindings) { + if (device.Target().registrationModules) break; Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "cube emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "cube emulated comparison retained an unused sampler association"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp index 77b95c736..ba1c7bb84 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp @@ -30,7 +30,7 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: const auto instructions = store ? std::span(storeCode) : std::span(code); ShaderRecompiler::RecompileRequest request{{ShaderStage::Compute, reinterpret_cast(instructions.data()), instructions, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, {}}, device.Target(), {0u, 0u, 0u, 128u}}; auto result = ShaderRecompiler::Recompile(request); - Require(result.runtimeImageCount == 0u, "direct image allocated runtime image metadata"); + Require(result.runtimeImageCount == (request.target.registrationModules ? 1u : 0u), "direct image runtime metadata does not follow the module mode"); Require(result.shaderDataDwords < ShaderRecompiler::RuntimeAbi::ShaderDataDwords, "shader data was not compacted"); for (const auto& binding : result.bindings) { if (binding.role == ShaderRecompiler::DescriptorRole::GuestImages) Require(binding.count == 1u, "single image expanded into unused descriptor slots"); @@ -48,6 +48,7 @@ void Run(AgcDriver::VulkanDevice& device) { device.Dispatch(shaders.back(), 1u, 1u, 1u); device.SubmitRecorded(); } + if (device.Target().registrationModules) Require(shaders[0].spirv == shaders[1].spirv && shaders[0].spirv == shaders[2].spirv, "registration modules: a present or a null image specialized the module"); shaders.clear(); device.WaitIdle(); for (std::uint32_t batch = 0u; batch < outputs.size(); ++batch) { diff --git a/core/shader/recompiler/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index b4e12b4b5..6f1b3a0ca 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -226,7 +226,7 @@ int main(int argc, char** argv) { std::cout << "ShaderInfoCollector::Collect OK\n"; BindingAllocator bindingAllocator; - const auto bindings = bindingAllocator.Allocate(program, request.layout); + const auto bindings = bindingAllocator.Allocate(program, request.layout, request.target.registrationModules); std::cout << "BindingAllocator::Allocate OK, bindings=" << bindings.bindings.size() << "\n"; std::cout << "ALL STAGES OK\n"; diff --git a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp index 120de5358..0944d2353 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp @@ -23,7 +23,7 @@ struct BindingAllocationResult : CompiledBindingLayout { class BindingAllocator { public: - [[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout) const; + [[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules = false) const; [[nodiscard]] const IrDescriptorBinding& FindBinding(const IrBindingLayout& layout, DescriptorBindingKind kind) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 224a452d9..d600e6fbb 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -14,6 +14,7 @@ struct PreparedDescriptorBinding { std::vector resources; std::vector identityImageSwizzle; std::vector samplerFilterElements; + std::vector inactive; std::size_t writtenBuffers = 0; std::size_t readOnlyBuffers = 0; }; diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index f99616a96..822cc38b7 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -68,7 +68,7 @@ bool usesGds(const IrProgram& program) { } -BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout) const { +BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules) const { IrProgramMetadata& metadata = program.Metadata(); if (!metadata.shaderInfoComplete || metadata.bindingLayoutComplete) { fail(metadata.shaderInfoComplete ? "shader binding layout failed: binding layout already allocated" @@ -102,7 +102,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin next.dispatchThreadLimit = info.dispatchThreadLimit; if (info.buffers.size() > RuntimeAbi::BufferCapacity || info.images.size() > RuntimeAbi::ImageCapacity || info.samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader binding layout exceeds runtime metadata capacity"); if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); - if (std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); + if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 32505b852..bb333af47 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -295,7 +295,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } } } - for (std::uint32_t index = 0; index < info.images.size(); ++index) { + for (std::uint32_t index = 0; index < info.images.size() && !registrationModules; ++index) { if (info.images[index].indirectRoot != ImageResource::NoIndirectImage) continue; const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); @@ -327,7 +327,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto first = (word >> 12u) & 0xfu; const auto last = (word >> 16u) & 0xfu; if (last < first || last - first >= RuntimeAbi::StorageMipSlots) fail("invalid dynamic storage mip range"); - plan.specialization.push_back({PipelineSpecialization::MipCountBase + index, last - first + 1u}); + if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::MipCountBase + index, last - first + 1u}); } for (std::uint32_t resource = 0; resource < info.images.size(); ++resource) { const auto& image = info.images[resource]; @@ -340,7 +340,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (mode != imageModes[slot]) fail("indirect image mode numbering disagrees with its root"); active[mode] = true; } - for (std::uint32_t mode = 0; mode < modes.size(); ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u}); + for (std::uint32_t mode = 0; mode < modes.size() && !registrationModules; ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u}); } plan.bindings.reserve(layout.descriptors.size()); for (const auto& logical : layout.descriptors) { @@ -348,6 +348,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const bool samplerHeap = logical.kind == DescriptorBindingKind::Samplers; IrDescriptorBinding compact{logical.kind, {}}; std::vector originals; + std::vector inactive; std::uint32_t previous = UINT32_MAX; std::uint32_t mip = 0; for (std::uint32_t element = 0; element < logical.resources.size(); ++element) { @@ -365,13 +366,14 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } else if (samplerHeap) { active = (samplerModes.at(resource) & (1u << (element & 1u))) != 0u; } - if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast(logical.kind), element), active ? static_cast(compact.resources.size()) : 0u}); - if (active) { + if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast(logical.kind), element), active ? static_cast(compact.resources.size()) : 0u}); + if (active || ((imageHeap || samplerHeap) && registrationModules)) { compact.resources.push_back(resource); originals.push_back(element); + inactive.push_back(!active); } } - if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast(logical.kind), std::max(1u, static_cast(compact.resources.size()))}); + if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast(logical.kind), std::max(1u, static_cast(compact.resources.size()))}); if ((imageHeap || samplerHeap) && compact.resources.empty()) continue; PreparedDescriptorBinding entry{}; auto& physical = entry.descriptor; @@ -406,7 +408,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.imageUnnormalizedBaseLevel.push_back(unnormalized.imagesAtBaseLevel.at(resource)); physical.imageSamplers.push_back(0u); physical.imageAtomic64.push_back(image.atomic64); - if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage) continue; + if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage || inactive[element]) continue; if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout"); const auto& descriptor = snapshot.images.at(resource); RuntimeAbi::ResourceMetadata metadata{}; @@ -428,20 +430,34 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.samplerUnnormalized.push_back(unnormalized.samplers.at(resource)); const auto mode = 1u << (originals[element] & 1u); physical.samplerIntegerBorder.push_back((samplerIntegerModes.at(resource) & mode) != 0u && (samplerFloatModes.at(resource) & mode) == 0u); - if ((originals[element] & 1u) != 0u) entry.samplerFilterElements.push_back(element); + if ((originals[element] & 1u) != 0u && !inactive[element]) entry.samplerFilterElements.push_back(element); } } else if (physical.role == DescriptorRole::ShaderData && layout.UsesPushData()) { fail("DescriptorBindingBuilder::Populate shader-data binding must not exist when push data is used"); } plan.storageData |= physical.role == DescriptorRole::ShaderData; entry.resources = std::move(compact.resources); + if (registrationModules && (imageHeap || samplerHeap)) entry.inactive = std::move(inactive); plan.bindings.push_back(std::move(entry)); } + for (std::uint32_t resource = 0; resource < info.images.size() && registrationModules && layout.runtimeImageCount != 0u; ++resource) { + if (info.images[resource].indirectRoot != ImageResource::NoIndirectImage) continue; + if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout"); + const auto& descriptor = snapshot.images.at(resource); + RuntimeAbi::ResourceMetadata metadata{}; + if (descriptor.dwordCount != metadata.descriptor.size()) fail("invalid runtime image descriptor width"); + metadata.elementCount = 1u; + metadata.flags = imageModes[resource] << 1u; + if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u; + plan.imageMetadata.push_back({resource, layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)), metadata}); + } std::vector samplerMasks(info.samplers.size()); for (const auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestSamplers) continue; if (binding.resources.size() > ShaderInfo::MaxSamplers) fail("prepared sampler mask capacity exceeded"); - for (std::uint32_t element = 0; element < binding.resources.size(); ++element) samplerMasks.at(binding.resources[element]) |= 1u << element; + for (std::uint32_t element = 0; element < binding.resources.size(); ++element) { + if (binding.inactive.empty() || !binding.inactive[element]) samplerMasks.at(binding.resources[element]) |= 1u << element; + } } for (auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestImages) continue; @@ -496,6 +512,10 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con for (std::size_t element = 0; element < entry.resources.size(); ++element) { const auto& descriptor = snapshot.images.at(entry.resources[element]); if (descriptor.dwordCount != 8u) fail("invalid runtime image descriptor width"); + if (!entry.inactive.empty() && entry.inactive[element]) { + physical.guestDescriptor.insert(physical.guestDescriptor.end(), 8u, 0u); + continue; + } const auto offset = physical.guestDescriptor.size(); physical.guestDescriptor.insert(physical.guestDescriptor.end(), descriptor.dwords.begin(), descriptor.dwords.begin() + 8); if (entry.identityImageSwizzle[element]) physical.guestDescriptor[offset + 3u] = (physical.guestDescriptor[offset + 3u] & ~0xfffu) | ShaderImageIdentitySwizzle; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 5fda2ba0b..8978f1851 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -406,7 +406,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra shaderInfoCollector.Collect(program, inputInfo); constexpr BindingAllocator bindingAllocator; - auto bindings = bindingAllocator.Allocate(program, request.layout); + auto bindings = bindingAllocator.Allocate(program, request.layout, request.target.registrationModules); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index 1650714e3..7f726e995 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -7,6 +7,7 @@ namespace ShaderRecompiler { +std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member); std::uint32_t ResourceForDescriptor(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource); std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t resultPtrType, std::uint32_t variableId, std::uint32_t arrayIndex, DescriptorBindingKind kind, std::uint32_t resource, const char* variableName); std::uint32_t ImageScalarType(SpirvEmitterState& state, IrTextureNumericClass numericClass); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 5e209712a..81806a48f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -4,6 +4,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include "SpirvBackend/SpirvMemory/SpirvDescriptors.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" #include @@ -46,18 +47,8 @@ struct SampleSetup { std::uint32_t coord; }; -std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) { - const auto index = state.runtimeImageMetadata != 0u ? state.runtimeImageMetadata : ConstantU32(state, resource); - const auto dword = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + member), Binary(state, spv::OpIMul, TypeU32(state), index, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)))); - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0u), dword); - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); - return value; -} - std::uint32_t RuntimeImageSwizzle(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t component) { - if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); + if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); const auto descriptor = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); return Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), descriptor, ConstantU32(state, component * 3u)), ConstantU32(state, 7u)); } @@ -1653,7 +1644,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { state.runtimeImageMetadata = 0u; return; } - const auto selector = base.indirectRoot == ImageResource::NoIndirectImage ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + const auto selector = base.indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); const bool returnsValue = inst.Opcode() != IrOpcode::ImageWrite; const auto resultId = returnsValue ? ctx.Result(inst) : 0u; ctx.definitions.erase(&inst); @@ -1677,7 +1668,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { for (std::uint32_t index = 0u; index < modes.size(); ++index) { EmitLabel(state, labels[index]); std::uint32_t modeMerge = 0u; - if (base.indirectRoot != ImageResource::NoIndirectImage) { + if (base.indirectRoot != ImageResource::NoIndirectImage && !state.registrationModules) { const auto enabled = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageModeBase + memory.resource * PipelineSpecialization::ImageModeStride + index, 1u); const auto active = Binary(state, spv::OpINotEqual, TypeBool(state), enabled, ConstantU32(state, 0u)); const auto activeLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index b1a37bb57..4989ed437 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -50,11 +50,28 @@ std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t r } const auto pointer = state.module.AllocateId(); const bool heap = kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(kind) != ImageResourceClass::None; - const auto index = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex); + const auto index = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex); state.module.AddFunction(spv::OpAccessChain, resultPtrType, pointer, variableId, index); return pointer; } +std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) { + const auto index = state.runtimeImageMetadata != 0u ? state.runtimeImageMetadata : ConstantU32(state, resource); + const auto dword = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + member), Binary(state, spv::OpIMul, TypeU32(state), index, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0u), dword); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; +} + +std::uint32_t RuntimeMipCount(SpirvEmitterState& state, std::uint32_t resource) { + const auto word = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); + const auto first = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 12u)), 0xfu); + const auto last = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 16u)), 0xfu); + return Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), last, first), ConstantU32(state, 1u)); +} + std::uint32_t TableImageIndex(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource) { const bool storage = ImageBindingResourceClass(kind) == ImageResourceClass::Storage; const auto supported = [&](std::uint32_t capability) { @@ -240,6 +257,7 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std if (image.mipCount == 0u) { ExitDescriptorBindingFailure(state, kind, resource, "dynamic storage image has no mip descriptors"); } + const auto count = state.registrationModules ? RuntimeMipCount(state, resource) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount); const auto mergeLabel = state.module.AllocateId(); std::vector labels(image.mipCount); std::vector words{spv::OpSwitch, mipLod, mergeLabel}; @@ -252,7 +270,6 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std state.module.AddFunction(std::span(words)); for (std::uint32_t mip = 0; mip < image.mipCount; mip++) { EmitLabel(state, labels[mip]); - const auto count = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount); const auto active = Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, mip), count); const auto writeLabel = state.module.AllocateId(); const auto nextLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index c484fa2b8..cc7ab59a8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -398,7 +398,7 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto ArrayType = [&](std::uint32_t type) { const bool heap = binding.kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(binding.kind) != ImageResourceClass::None; const auto count = static_cast(binding.resources.size()); - const auto length = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast(binding.kind), count) : ConstantU32(state, count); + const auto length = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast(binding.kind), count) : ConstantU32(state, count); return state.module.Type(spv::OpTypeArray, type, length); }; switch (binding.kind) { From baf34374657f665ffcb05325fe8f83ee1e35837c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 13:53:14 +0200 Subject: [PATCH 282/318] perf(recompiler): read depth-compare state and export mappings at run time when modules are built at registration Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 4 +-- .../execution/BufferDescriptorRuntime.cpp | 8 +++-- .../tests/execution/DepthFastClearHarness.hpp | 5 +-- .../tests/execution/PreparedExports.cpp | 8 +++-- .../tests/execution/ProvokingVertex.cpp | 4 +-- .../tests/execution/RecordedKeptBytes.cpp | 4 +-- .../tests/execution/VertexFetch.cpp | 4 +-- .../IrMetadata/DescriptorBinding.hpp | 6 +++- .../Optimization/DescriptorBindingBuilder.hpp | 1 + .../Optimization/src/BindingAllocator.cpp | 1 + .../src/DescriptorBindingBuilder.cpp | 26 ++++++++++---- core/shader/recompiler/Recompiler.cpp | 35 ++++++++++++++++++- core/shader/recompiler/RuntimeAbi.hpp | 6 ++++ core/shader/recompiler/ShaderDiskCache.cpp | 2 ++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 16 +++++++-- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- 16 files changed, 105 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 99955da54..29891d598 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -184,13 +184,13 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {64, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush}, + target.fixedPushSlots ? PixelPushLayout(meshPush, target) : ShaderRecompiler::BindingLayout{0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush}, std::nullopt }; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Mesh, &meshResult, 0}, - {ShaderStage::Fragment, &pixelResult, meshPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(meshPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index 1948f7e20..eafac530e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -73,6 +73,7 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { {"absent", 0, 128, false}, }}; const void* first = nullptr; + std::uint64_t variant = 0; for (const auto& test : cases) { Output.fill(Fill); const auto input = test.present ? BufferDescriptor(Input.data() + test.baseOffset, test.records) : std::array{0u, 0u, 0u, 0x31016facu}; @@ -83,8 +84,11 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { runtime.userData = userData; const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); const auto result = ShaderRecompiler::MaterializeShader(request, *capture, *handle); - if (first == nullptr) first = result->spirv.data(); - if (registrationModules) Require(result->spirv.data() == first, "buffer descriptor runtime, " + mode + ", " + test.name + ": the V# specialized the module"); + if (first == nullptr) { + first = result->spirv.data(); + variant = result->PipelineVariantId(); + } + if (registrationModules) Require(result->spirv.data() == first && result->PipelineVariantId() == variant, "buffer descriptor runtime, " + mode + ", " + test.name + ": the V# specialized the module"); device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); for (std::uint32_t lane = 0; lane < Lanes; ++lane) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index 18d3083b4..1e4e5bcb3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" #include #include #include @@ -96,7 +97,7 @@ inline Shaders Compile(AgcDriver::VulkanDevice& device) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, shaders.vertexPush, 128 - shaders.vertexPush} + PixelPushLayout(shaders.vertexPush, target) }; fragment.useCache = false; shaders.pixel = ShaderRecompiler::Recompile(fragment); @@ -107,7 +108,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil Pixels.fill(std::byte{0}); const std::array shaders{{ {ShaderStage::Vertex, &compiled.vertex, 0}, - {ShaderStage::Fragment, &compiled.pixel, compiled.vertexPush} + {ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp index b0b85a7b7..bdbfd1362 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp @@ -29,7 +29,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { const std::array values = integer ? std::array{11, 22, 33, 44} : std::array{0, 0x3e800000u, 0x3f000000u, 0x3f800000u}; const std::array code{0x7e0002ffu, values[0], 0x7e0202ffu, values[1], 0x7e0402ffu, values[2], 0x7e0602ffu, values[3], 0xf800100fu, 0x03020100u, 0xf800183fu, 0x03020100u, 0xbf810000u}; AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, integer ? 0x7007u : 0x9009u}}; - RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), {0, 0, 0, 128}}; + RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), PixelPushLayout(0u, device.Target())}; const auto handle = PrepareShader(request); const auto& artifact = GetPreparedArtifact(*handle); std::uint32_t scalarType = 0; @@ -85,6 +85,10 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { request.useCache = cached; const auto materialized = MaterializeShader(request, *capture, *handle); Require(materialized->spirv.data() == pixel->spirv.data() && materialized->PipelineVariantId() == pixel->PipelineVariantId(), "export mapping did not reuse its specialized module"); + if (device.Target().registrationModules) { + Require(materialized->PipelineVariantId() == first->PipelineVariantId(), "registration modules: the export mapping specialized the fragment module"); + continue; + } for (std::size_t cursor = 5; cursor < materialized->spirv.size();) { const auto count = materialized->spirv[cursor] >> 16u; Require(count != 0u && count <= materialized->spirv.size() - cursor, "invalid specialized fragment instruction"); @@ -94,7 +98,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { } request.useCache = true; if (integer) continue; - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), 0}}}; + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), PixelPushOffset(0u, device.Target())}}}; pixels.fill(0xdeadbeef); otherPixels.fill(0xdeadbeef); device.Draw(state, draw, shaders); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp index f1651a590..2654a113b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp @@ -50,7 +50,7 @@ ShaderRecompiler::RecompileResult GuestPixel(const ShaderRecompiler::SpirvTarget request.context.waveSize = 64; request.context.pixel = pixel; request.target = target; - request.layout.pushConstantSizeBytes = 128; + request.layout = PixelPushLayout(0u, target); request.useCache = false; auto result = ShaderRecompiler::Recompile(request); result.variantId = 3; @@ -130,7 +130,7 @@ int main() { const auto guestPixel = GuestPixel(device->Target()); const std::array guestShaders{{ {ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, - {ShaderRecompiler::ShaderStage::Fragment, &guestPixel, 0} + {ShaderRecompiler::ShaderStage::Fragment, &guestPixel, PixelPushOffset(0u, device->Target())} }}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP}) Run(*device, topology, guestShaders, true, "guest P0"); std::puts("provoking vertex rendering tests passed: GLSL and guest P0, triangle list and strip, first/last/first"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp index a41fe4456..f6e0f04fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp @@ -200,13 +200,13 @@ void Draw(AgcDriver::VulkanDevice& device, GuestBuffer& pixels, GuestBuffer& ver {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp index 4baa2d05b..c42f5c35c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp @@ -85,9 +85,9 @@ void Run(AgcDriver::VulkanDevice& device) { pixelInfo.wave32 = true; pixelInfo.targetOutputMode[0] = 9u; pixelInfo.targetExportMapping.fill(0xe4u); - ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}}; + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; const auto pixel = ShaderRecompiler::Recompile(pixelRequest); - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}}; + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index c1a9500b7..976a74f8e 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -43,6 +43,7 @@ struct IrBindingLayout { std::uint32_t memoryOffsetCount = 0; bool dispatchThreadLimit = false; std::uint32_t runtimeImageCount = 0; + std::uint32_t runtimeStateDwords = 0; std::vector userDataRegisters; std::vector descriptors; @@ -52,9 +53,12 @@ struct IrBindingLayout { [[nodiscard]] std::uint32_t ImageMetadataDword() const { return DispatchThreadLimitDword() + (dispatchThreadLimit ? 3u : 0u); } - [[nodiscard]] std::uint32_t ShaderDataDwords() const { + [[nodiscard]] std::uint32_t RuntimeStateDword() const { return ImageMetadataDword() + runtimeImageCount * (sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)); } + [[nodiscard]] std::uint32_t ShaderDataDwords() const { + return RuntimeStateDword() + runtimeStateDwords; + } [[nodiscard]] bool UsesPushData() const { return pushDataStartDword != PushData::NoStart; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index d600e6fbb..6c15439dc 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -29,6 +29,7 @@ struct DescriptorBindingPlan { std::vector bindings; std::vector imageMetadata; std::vector specialization; + std::vector runtimeState; bool storageData = false; }; diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 822cc38b7..268265461 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -103,6 +103,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin if (info.buffers.size() > RuntimeAbi::BufferCapacity || info.images.size() > RuntimeAbi::ImageCapacity || info.samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader binding layout exceeds runtime metadata capacity"); if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); + if (registrationModules && (program.Resources().stage == IrShaderStage::Pixel || std::ranges::any_of(info.samplers, [](const auto& sampler) { return FoldsTexelOffsets(sampler); }))) next.runtimeStateDwords = RuntimeAbi::StateDwords; const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index bb333af47..851d92ed5 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -208,7 +208,7 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh return proof; } -std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const std::vector& imageMetadata) { +std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const std::vector& imageMetadata, std::span runtimeState) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); ++i) { const auto reg = layout.userDataRegisters[i]; @@ -226,6 +226,8 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st std::copy_n(descriptor.dwords.begin(), metadata.descriptor.size(), metadata.descriptor.begin()); std::memcpy(result.data() + entry.offset, &metadata, sizeof(metadata)); } + if (runtimeState.size() != layout.runtimeStateDwords) fail("runtime state does not match its shader data layout"); + std::copy(runtimeState.begin(), runtimeState.end(), result.begin() + layout.RuntimeStateDword()); return result; } @@ -255,8 +257,11 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; const auto unnormalized = ProveUnnormalized(info, snapshot); + std::uint32_t unnormalizedSamplers = 0u; for (std::uint32_t index = 0; index < info.samplers.size(); ++index) { - if (FoldsTexelOffsets(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); + if (!FoldsTexelOffsets(info.samplers[index])) continue; + if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); + else if (unnormalized.samplers[index]) unnormalizedSamplers |= 1u << index; } std::vector compareStates(info.images.size()); for (std::uint32_t index = 0; index < info.images.size(); ++index) { @@ -264,7 +269,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto compare = compareStates[index] = ResourceMaterializer::EmulatedCompareState(info, snapshot, index); const auto first = PipelineSpecialization::CompareBase + index * PipelineSpecialization::CompareWords; const std::array values{EmulatedCompare::Function(compare), (compare & EmulatedCompare::Linear) != 0u, EmulatedCompare::AddressX(compare), EmulatedCompare::AddressY(compare), EmulatedCompare::Reference(compare), (compare & EmulatedCompare::BorderWhite) != 0u}; - for (std::uint32_t word = 0; word < values.size(); ++word) plan.specialization.push_back({first + word, values[word]}); + for (std::uint32_t word = 0; word < values.size() && !registrationModules; ++word) plan.specialization.push_back({first + word, values[word]}); } std::vector imageModes(info.images.size()); for (std::size_t index = 0; index < info.images.size(); ++index) imageModes[index] = ResourceMaterializer::RuntimeImageMode(info.images[index], snapshot.images.at(index), info.runtimeImageModes.at(index)); @@ -301,7 +306,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); } - for (std::uint32_t target = 0; target < exportMappings.size(); ++target) { + for (std::uint32_t target = 0; target < exportMappings.size() && !registrationModules; ++target) { for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({PipelineSpecialization::ExportBase + target * 4u + component, (exportMappings[target] >> (component * 2u)) & 3u}); } std::vector samplerModes(info.samplers.size(), 0u); @@ -416,7 +421,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l metadata.binding = static_cast(compact.kind); metadata.firstElement = static_cast(element); metadata.elementCount = 1u; - metadata.flags = imageModes[resource] << 1u; + metadata.flags = imageModes[resource] << 1u | (registrationModules ? (compareStates[resource] & RuntimeAbi::CompareStateMask) << RuntimeAbi::CompareStateShift : 0u); if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u; const auto offset = layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)); plan.imageMetadata.push_back({resource, offset, metadata}); @@ -447,10 +452,16 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l RuntimeAbi::ResourceMetadata metadata{}; if (descriptor.dwordCount != metadata.descriptor.size()) fail("invalid runtime image descriptor width"); metadata.elementCount = 1u; - metadata.flags = imageModes[resource] << 1u; + if (imageModes[resource] > RuntimeAbi::ImageModeMask) fail("runtime image mode exceeds its metadata field"); + metadata.flags = imageModes[resource] << 1u | (registrationModules ? (compareStates[resource] & RuntimeAbi::CompareStateMask) << RuntimeAbi::CompareStateShift : 0u); if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u; plan.imageMetadata.push_back({resource, layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)), metadata}); } + if (layout.runtimeStateDwords != 0u) { + plan.runtimeState.assign(layout.runtimeStateDwords, 0u); + plan.runtimeState.at(RuntimeAbi::StateUnnormalizedSamplers) = unnormalizedSamplers; + for (std::uint32_t target = 0; target < exportMappings.size(); ++target) plan.runtimeState.at(RuntimeAbi::StateExportMappings + target / 4u) |= static_cast(exportMappings[target]) << (target % 4u * 8u); + } std::vector samplerMasks(info.samplers.size()); for (const auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestSamplers) continue; @@ -488,13 +499,14 @@ DescriptorBindingPlan DescriptorBindingBuilder::Select(const DescriptorBindingPl } } selected.imageMetadata = plan.imageMetadata; + selected.runtimeState = plan.runtimeState; return selected; } void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { const auto& layout = compiled.layout; const bool needsShaderData = layout.UsesPushData() || plan.storageData; - auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata) : std::vector{}; + auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata, plan.runtimeState) : std::vector{}; std::vector bindings; bindings.reserve(plan.bindings.size()); std::size_t writtenHere = 0; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 8978f1851..f7d2ff5b2 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -12,6 +12,7 @@ #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "ShaderDiskCache.hpp" #include +#include #include #include #include @@ -602,6 +603,38 @@ struct PreparedBindingPlan { std::map, std::shared_ptr> modules; }; +struct RegistrationModuleEntry { + std::once_flag ready; + std::exception_ptr failure; + std::shared_ptr module; +}; + +std::shared_ptr registrationModule(std::uint64_t variantId, std::span moduleKey, const std::function()>& specialize) { + std::vector key{variantId}; + key.insert(key.end(), moduleKey.begin(), moduleKey.end()); + static std::shared_mutex mutex; + static std::map, std::shared_ptr> modules; + std::shared_ptr entry; + { + std::shared_lock lock(mutex); + if (const auto found = modules.find(key); found != modules.end()) entry = found->second; + } + if (entry == nullptr) { + std::unique_lock lock(mutex); + const auto found = modules.find(key); + entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared()).first->second; + } + std::call_once(entry->ready, [&] { + try { + entry->module = specialize(); + } catch (...) { + entry->failure = std::current_exception(); + } + }); + if (entry->failure) std::rethrow_exception(entry->failure); + return entry->module; +} + std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool registrationModules) { struct BindingPlanKeyStorage {}; auto& key = HostThreadLocal, BindingPlanKeyStorage>(); @@ -769,7 +802,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil constants.push_back({first + 5u, kind}); } } - const auto module = specializeModule(variant.artifact, vertexClasses, constants, request.target); + const auto module = request.target.registrationModules && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); if (variant.bindings.layout.UsesPushData() && !module->pushData) throw std::runtime_error("specialization removed the prepared push constant interface"); auto selected = DescriptorBindingBuilder{}.Select(plan->bindings, module->bindings); entry->bindings = std::move(selected); diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 131372e68..8d5019b93 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -39,6 +39,12 @@ inline constexpr std::uint32_t BindlessTableSlots = 16u; inline constexpr std::uint32_t StorageMipSlots = 4u; inline constexpr std::uint32_t StorageHeapCapacity = 16u; inline constexpr std::uint32_t SamplerHeapCapacity = 32u; +inline constexpr std::uint32_t StateUnnormalizedSamplers = 0u; +inline constexpr std::uint32_t StateExportMappings = 1u; +inline constexpr std::uint32_t StateDwords = 3u; +inline constexpr std::uint32_t ImageModeMask = 0xffu; +inline constexpr std::uint32_t CompareStateShift = 9u; +inline constexpr std::uint32_t CompareStateMask = 0x1fffu; struct ResourceMetadata { std::uint32_t binding; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 203770688..0949aef04 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -416,6 +416,7 @@ void encodeLayout(Writer& writer, const IrBindingLayout& layout) { writer.Value(layout.memoryOffsetCount); writer.Value(layout.dispatchThreadLimit); writer.Value(layout.runtimeImageCount); + writer.Value(layout.runtimeStateDwords); writer.Values(std::span(layout.userDataRegisters)); writer.List(layout.descriptors, [](Writer& out, const IrDescriptorBinding& descriptor) { out.Value(descriptor.kind); @@ -429,6 +430,7 @@ void decodeLayout(Reader& reader, IrBindingLayout& layout) { reader.Value(layout.memoryOffsetCount); reader.Value(layout.dispatchThreadLimit); reader.Value(layout.runtimeImageCount); + reader.Value(layout.runtimeStateDwords); reader.Values(layout.userDataRegisters); reader.List(layout.descriptors, 12, [](Reader& in, IrDescriptorBinding& descriptor) { in.Value(descriptor.kind); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 81806a48f..7297e528d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -47,6 +47,11 @@ struct SampleSetup { std::uint32_t coord; }; +std::uint32_t RuntimeImageMode(SpirvEmitterState& state, std::uint32_t resource) { + const auto mode = Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + return state.registrationModules ? Binary(state, spv::OpBitwiseAnd, TypeU32(state), mode, ConstantU32(state, RuntimeAbi::ImageModeMask)) : mode; +} + std::uint32_t RuntimeImageSwizzle(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t component) { if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); const auto descriptor = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); @@ -1264,7 +1269,12 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess auto& state = ctx.state; const auto& mem = access.mem; const auto& image = access.image; - const auto parameter = [&](std::uint32_t word) { return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u); }; + const auto compare = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, mem.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, RuntimeAbi::CompareStateShift)), RuntimeAbi::CompareStateMask) : 0u; + const auto parameter = [&](std::uint32_t word) { + if (!state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u); + constexpr std::array, 6> fields{{{EmulatedCompare::FunctionShift, 7u}, {4u, 1u}, {EmulatedCompare::ClampXShift, 3u}, {EmulatedCompare::ClampYShift, 3u}, {EmulatedCompare::ReferenceShift, 3u}, {12u, 1u}}}; + return EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), compare, ConstantU32(state, fields.at(word).first)), fields.at(word).second); + }; const auto equal = [&](std::uint32_t value, std::uint32_t literal) { return Binary(state, spv::OpIEqual, TypeBool(state), value, ConstantU32(state, literal)); }; if (access.slot != 0) ctx.Fail(access.inst, "compares against a color texture through a bindless image table, which is not implemented"); if (HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod)) ctx.Fail(access.inst, "compares against a color texture with gradients or an explicit LOD, which is not implemented"); @@ -1484,7 +1494,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con } else if (!offsetOperand) { sample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands); } else { - const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); + const auto selector = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateUnnormalizedSamplers), ConstantU32(state, mem.sampler)), 1u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); const auto folded = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); const auto foldLabel = state.module.AllocateId(); const auto operandLabel = state.module.AllocateId(); @@ -1644,7 +1654,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { state.runtimeImageMetadata = 0u; return; } - const auto selector = base.indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + const auto selector = base.indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : RuntimeImageMode(state, memory.resource); const bool returnsValue = inst.Opcode() != IrOpcode::ImageWrite; const auto resultId = returnsValue ? ctx.Result(inst) : 0u; ctx.definitions.erase(&inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1793ea2f8..ac2cc1366 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1319,7 +1319,7 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if (exp.index >= 8u) throw std::runtime_error("fragment export target exceeds the runtime mapping table"); std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { - const auto index = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); + const auto index = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateExportMappings + exp.index / 4u), ConstantU32(state, exp.index % 4u * 8u + component * 2u)), 3u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); components[component] = state.module.AllocateId(); state.module.AddFunction(spv::OpVectorExtractDynamic, uintOutput ? TypeU32(state) : TypeF32(state), components[component], value, index); } From 4d0b5c78984fbaa620acd39985e623ca8459cb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 14:50:51 +0200 Subject: [PATCH 283/318] perf(agc): give the fragment stage its own descriptor set when modules are built at registration Co-Authored-By: Claude --- .../Graphics/include/PipelineLibrary.hpp | 2 + .../Graphics/include/ShaderResources.hpp | 16 +++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 4 +- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 33 +++++- .../Graphics/src/PipelineLibrary.cpp | 27 +++-- .../Graphics/src/ShaderResources.cpp | 111 ++++++++++++++---- .../Graphics/src/ShaderValidation.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 2 +- .../Optimization/src/BindingAllocator.cpp | 2 +- .../src/DescriptorBindingBuilder.cpp | 8 +- core/shader/recompiler/RuntimeAbi.hpp | 6 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 2 +- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- 13 files changed, 164 insertions(+), 53 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index f480042a3..4ffb5e3bc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -19,6 +19,8 @@ struct PipelineLibraryKeys { std::vector fragmentOutput; std::vector renderPass; std::vector layout; + std::vector preRasterizationLayout; + std::vector fragmentLayout; }; std::span PipelineLibraryDynamicStates(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 014a06035..4e4cf13b4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -117,6 +117,8 @@ public: std::vector LayoutBindings() const; bool LayoutUpdateAfterBind() const; std::span LayoutBindingFlags() const { return layoutBindingFlags; } + VkDescriptorSetLayout FragmentLayout() const { return fragment.layout; } + bool SplitSets() const { return splitSets; } void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; struct DrawBindings { DrawBindings() = default; @@ -127,6 +129,7 @@ public: std::shared_ptr buffer; }; DescriptorCache* cache = nullptr; + DescriptorCache::SetAllocation fragmentAllocation; DescriptorCache::SetAllocation allocation; std::vector snapshots; ~DrawBindings(); @@ -165,6 +168,7 @@ public: bool ReadsImage(const StorageTexture* image) const; bool HasCachedStorageImages() const; const std::vector& LayoutKey() const { return layoutKey; } + const std::vector& FragmentLayoutKey() const { return fragmentLayoutKey; } // Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero. std::string Describe() const; @@ -428,6 +432,17 @@ private: VkDescriptorPool pool = VK_NULL_HANDLE; // The cache pool the set was allocated from, freed back to it on release. DescriptorCache::SetAllocation cacheAllocation; + struct SetObjects { + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + bool ownsLayout = false; + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + DescriptorCache::SetAllocation cacheAllocation; + }; + bool splitSets = false; + std::vector fragmentLayoutKey; + SetObjects fragment; + std::uint32_t setOf(const Binding& binding) const; std::vector allocations; std::vector descriptorBuffers; std::vector descriptorImages; @@ -479,6 +494,7 @@ private: // the descriptor counts the set was sized for, and the compute stage of a deferred build. std::vector bindings; std::vector layoutBindingFlags; + std::vector fragmentBindingFlags; std::vector refreshResourceKey; struct DeferredImages { const ShaderRecompiler::DescriptorBinding* binding; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 2f3bf711d..914e7b269 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1764,7 +1764,9 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); if (drawBindings != nullptr) { const auto set = drawBindings->allocation.set; - context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr); + const auto fragmentSet = drawBindings->fragmentAllocation.set; + if (set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr); + if (fragmentSet != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragmentSet, 0, nullptr); } else { resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout()); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 139cff586..2bcd134d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -217,8 +217,28 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); - appendKey(keys.layout, pushStagesOf(context)); - for (const auto word : resources.LayoutKey()) appendKey(keys.layout, word); + if (!resources.SplitSets()) { + appendKey(keys.layout, pushStagesOf(context)); + for (const auto word : resources.LayoutKey()) appendKey(keys.layout, word); + keys.preRasterizationLayout = keys.fragmentLayout = keys.layout; + return true; + } + const auto splitKey = [&](std::vector& key, std::uint32_t sets) { + appendKey(key, ~0u); + appendKey(key, sets); + appendKey(key, pushStagesOf(context)); + if ((sets & 1u) != 0u) { + appendKey(key, resources.LayoutKey().size()); + for (const auto word : resources.LayoutKey()) appendKey(key, word); + } + if ((sets & 2u) != 0u) { + appendKey(key, resources.FragmentLayoutKey().size()); + for (const auto word : resources.FragmentLayoutKey()) appendKey(key, word); + } + }; + splitKey(keys.layout, 3u); + splitKey(keys.preRasterizationLayout, 1u); + splitKey(keys.fragmentLayout, 2u); return true; } @@ -280,12 +300,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } // A descriptor set layout with the same bindings as this one is compatible with the pipeline // layout, so later draws bind their own ShaderResources' set under it. - const auto setLayout = resources.Layout(); + const std::array setLayouts{resources.Layout(), resources.FragmentLayout()}; const VkPushConstantRange push{pushStages, 0, PipelinePushSlotBytes}; const std::array slots{{{pushStagesOf(context), 0, PipelinePushSlotBytes}, {VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes}}}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = 1; - layoutInfo.pSetLayouts = &setLayout; + layoutInfo.flags = resources.SplitSets() ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; + layoutInfo.setLayoutCount = resources.SplitSets() ? 2u : 1u; + layoutInfo.pSetLayouts = setLayouts.data(); layoutInfo.pushConstantRangeCount = dynamicRendering ? static_cast(slots.size()) : pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = dynamicRendering ? slots.data() : pushStages != 0 ? &push : nullptr; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout graphics"); @@ -709,6 +730,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat } append(key, resources.LayoutKey().size()); for (const auto word : resources.LayoutKey()) append(key, word); + append(key, resources.FragmentLayoutKey().size()); + for (const auto word : resources.FragmentLayoutKey()) append(key, word); append(key, state.hasColorTarget); append(key, state.rectList); append(key, state.topology); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index edb90c69e..df499bb21 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { @@ -111,12 +112,22 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip device.destroyLayout = context.Function("vkDestroyPipelineLayout"); device.destroyModule = context.Function("vkDestroyShaderModule"); } - auto pipelineLayout = device.layouts.find(keys.layout); - if (pipelineLayout == device.layouts.end()) { + const auto layoutFor = [&](const Key& key, std::optional set) { + auto found = device.layouts.find(key); + if (found != device.layouts.end()) return found->second; + auto info = layout; + std::array sets{VK_NULL_HANDLE, VK_NULL_HANDLE}; + if (set) { + sets.at(*set) = layout.pSetLayouts[*set]; + info.pSetLayouts = sets.data(); + } VkPipelineLayout handle = VK_NULL_HANDLE; - Check(context.Function("vkCreatePipelineLayout")(context.device, &layout, nullptr, &handle), "vkCreatePipelineLayout library"); - pipelineLayout = device.layouts.emplace(keys.layout, handle).first; - } + Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &handle), "vkCreatePipelineLayout library"); + return device.layouts.emplace(key, handle).first->second; + }; + const bool split = (layout.flags & VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT) != 0; + const auto fullLayout = layoutFor(keys.layout, std::nullopt); + const std::array partLayouts{VK_NULL_HANDLE, split ? layoutFor(keys.preRasterizationLayout, 0u) : fullLayout, split ? layoutFor(keys.fragmentLayout, 1u) : fullLayout, VK_NULL_HANDLE}; std::vector preRasterization; const VkPipelineShaderStageCreateInfo* fragment = nullptr; bool mesh = false; @@ -135,7 +146,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip }; const std::array partKeys{&keys.vertexInput, &keys.preRasterization, &keys.fragmentShader, &keys.fragmentOutput}; const Key none; - const std::array contextKeys{&none, &keys.layout, &keys.layout, &keys.renderPass}; + const std::array contextKeys{&none, &keys.preRasterizationLayout, &keys.fragmentLayout, &keys.renderPass}; const VkPipelineRenderingCreateInfoKHR shaderRendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; VkPipelineDepthStencilStateCreateInfo dynamicDepth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; static const bool backgroundLinking = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; @@ -155,7 +166,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &library}; create.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT; create.pDynamicState = info.pDynamicState; - create.layout = pipelineLayout->second; + create.layout = partLayouts[part]; switch (flags[part]) { case VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT: create.pVertexInputState = info.pVertexInputState; @@ -212,7 +223,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip *optimized = slot; const auto createPipelines = context.Function("vkCreateGraphicsPipelines"); const auto cache = context.pipelineCache; - const auto libraryLayout = pipelineLayout->second; + const auto libraryLayout = fullLayout; enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] { if (slot->released.load()) return; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 6c1d10286..e223a6d45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1166,6 +1166,14 @@ VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span PoolSizes(std::span bindings) { + std::map counts; + for (const auto& binding : bindings) counts[binding.descriptorType] += binding.descriptorCount; + std::vector sizes; + for (const auto& [type, count] : counts) sizes.push_back({type, count}); + return sizes; +} + VkDescriptorPool CreateSetPool(const Context& context, std::span sizes, bool updateAfterBind) { VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; info.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0; @@ -1339,7 +1347,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons updateAfterBind = true; }; for (const auto& binding : shader.program->bindings) { - Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero"); + Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shader.stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "unexpected descriptor set: a shader resource must use its stage's descriptor set"); Require(occupied.insert(binding.binding).second, "duplicate shader binding"); const bool addressRole = binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer; const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding.role == ShaderRecompiler::DescriptorRole::Gds; @@ -1416,23 +1424,62 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (drawBuild && !usesBda && std::all_of(allocations.begin(), allocations.end(), [&](const Allocation& allocation) { return !allocation.guest || allocation.pushByte >= 0 || (allocation.dataAllocation >= 0 && allocation.dataByte < allocations[static_cast(allocation.dataAllocation)].size); })) guestMemory.AllowAdjustedRegions(); guestMemory.UploadPrepare(usesBda); timing.uploadMs = phase(BuildPhase::Upload); - std::vector description; + splitSets = std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const auto& binding) { return binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; }); }); + std::array, 2> descriptions; for (const auto& binding : bindings) { - description.push_back(binding.layout); - layoutBindingFlags.push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); - layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); + const auto set = setOf(binding); + descriptions[set].push_back(binding.layout); + (set == 0 ? layoutBindingFlags : fragmentBindingFlags).push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + auto& key = set == 0 ? layoutKey : fragmentLayoutKey; + key.insert(key.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); + } + if (updateAfterBind) { + layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + if (splitSets) fragmentLayoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); } - if (updateAfterBind) layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr; static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr; - if (context.descriptorCache != nullptr && !noLayoutCache) { - _layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind, layoutBindingFlags); - } else { + const auto createLayout = [&](const std::vector& key, const std::vector& description, const std::vector& flags, bool& owns) { + if (context.descriptorCache != nullptr && !noLayoutCache) return context.descriptorCache->Layout(key, description, updateAfterBind, flags); ValidateDescriptorLayout(context, description, updateAfterBind); - _layout = CreateSetLayout(context, description, updateAfterBind, layoutBindingFlags); - ownsLayout = true; + owns = true; + return CreateSetLayout(context, description, updateAfterBind, flags); + }; + _layout = createLayout(layoutKey, descriptions[0], layoutBindingFlags, ownsLayout); + if (splitSets) { + fragment.layout = createLayout(fragmentLayoutKey, descriptions[1], fragmentBindingFlags, fragment.ownsLayout); + if (!descriptions[1].empty()) { + const auto sizes = PoolSizes(descriptions[1]); + if (context.descriptorCache != nullptr && !noPoolCache) { + fragment.cacheAllocation = context.descriptorCache->Allocate(fragment.layout, sizes, updateAfterBind); + fragment.set = fragment.cacheAllocation.set; + } + if (fragment.set == VK_NULL_HANDLE) { + fragment.pool = CreateSetPool(context, sizes, updateAfterBind); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = fragment.pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &fragment.layout; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &fragment.set), "vkAllocateDescriptorSets"); + } + } } - if (!bindings.empty()) { + if (splitSets && !descriptions[0].empty()) { + const auto sizes = PoolSizes(descriptions[0]); + if (context.descriptorCache != nullptr && !noPoolCache) { + cacheAllocation = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind); + _set = cacheAllocation.set; + } + if (_set == VK_NULL_HANDLE) { + pool = CreateSetPool(context, sizes, updateAfterBind); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &_layout; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &_set), "vkAllocateDescriptorSets"); + } + } + if (!splitSets && !bindings.empty()) { // The set is sized from the plan: every image element becomes exactly one descriptor // when stage B looks it up. std::vector sizes; @@ -1492,7 +1539,7 @@ void ShaderResources::buildComplete() { if (usesBda) bda = std::make_unique(context, guestMemory); else if (usesFaultBuffer) bda = std::make_unique(context); phase(BuildPhase::Bda); - if (_set != VK_NULL_HANDLE) { + if (_set != VK_NULL_HANDLE || fragment.set != VK_NULL_HANDLE) { // One update call for the whole set: the info arrays are sized up front so every write's // pointer into them stays valid until the call. std::size_t bufferCount = 0; @@ -1514,7 +1561,7 @@ void ShaderResources::buildComplete() { }; for (const auto& binding : bindings) { VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - write.dstSet = _set; + write.dstSet = setOf(binding) == 0 ? _set : fragment.set; write.dstBinding = binding.layout.binding; write.descriptorCount = binding.layout.descriptorCount; write.descriptorType = binding.layout.descriptorType; @@ -3334,10 +3381,19 @@ void ShaderResources::release() noexcept { if (cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(cacheAllocation); if (pool) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); if (_layout && ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, _layout, nullptr); + if (fragment.cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(fragment.cacheAllocation); + if (fragment.pool) context.Function("vkDestroyDescriptorPool")(context.device, fragment.pool, nullptr); + if (fragment.layout && fragment.ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, fragment.layout, nullptr); cacheAllocation = {}; pool = VK_NULL_HANDLE; _set = VK_NULL_HANDLE; _layout = VK_NULL_HANDLE; + fragment = {}; +} + +std::uint32_t ShaderResources::setOf(const Binding& binding) const { + constexpr auto count = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + return splitSets && binding.layout.binding / count == static_cast(ShaderRecompiler::RuntimeAbi::Stage::Fragment) ? 1u : 0u; } VkDescriptorSetLayout ShaderResources::Layout() const { @@ -3356,11 +3412,12 @@ bool ShaderResources::LayoutUpdateAfterBind() const { ShaderResources::DrawBindings::~DrawBindings() { if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); + if (cache != nullptr && fragmentAllocation.set != VK_NULL_HANDLE) cache->Free(fragmentAllocation); } std::optional> ShaderResources::MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const { std::vector moved; - if (_set == VK_NULL_HANDLE || usesBda) return moved; + if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return moved; for (const auto& shader : shaders) { if (shader.program == nullptr) return std::nullopt; for (const auto& binding : shader.program->bindings) { @@ -3413,7 +3470,7 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() { } std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { - if (_set == VK_NULL_HANDLE || usesBda) return {}; + if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); const auto maxSnapshotBytes = MaxDrawSnapshotBytes(); std::size_t totalSnapshotBytes = 0; @@ -3460,13 +3517,17 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } if (selected.empty()) return {}; Require(context.descriptorCache != nullptr, "draw snapshots require a descriptor cache"); - std::map counts; - for (const auto& binding : bindings) counts[binding.layout.descriptorType] += binding.layout.descriptorCount; - std::vector sizes; - for (const auto& [type, count] : counts) sizes.push_back({type, count}); + std::array, 2> descriptions; + for (const auto& binding : bindings) descriptions[setOf(binding)].push_back(binding.layout); result->cache = context.descriptorCache; - result->allocation = result->cache->Allocate(_layout, sizes); - Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + if (!descriptions[0].empty()) { + result->allocation = result->cache->Allocate(_layout, PoolSizes(descriptions[0])); + Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + } + if (!descriptions[1].empty()) { + result->fragmentAllocation = result->cache->Allocate(fragment.layout, PoolSizes(descriptions[1])); + Require(result->fragmentAllocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + } auto buffers = descriptorBuffers; auto writes = descriptorWrites; std::vector snapshotIndices(allocations.size(), selected.size()); @@ -3474,7 +3535,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi for (std::size_t index = 0; index < bindings.size(); ++index) { const auto& binding = bindings[index]; auto& write = writes[index]; - write.dstSet = result->allocation.set; + write.dstSet = setOf(binding) == 0 ? result->allocation.set : result->fragmentAllocation.set; if (binding.allocations.empty()) continue; const auto offset = static_cast(write.pBufferInfo - descriptorBuffers.data()); write.pBufferInfo = buffers.data() + offset; @@ -3492,8 +3553,8 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } void ShaderResources::Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const { - if (_set == VK_NULL_HANDLE) return; - context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); + if (_set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); + if (fragment.set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragment.set, 0, nullptr); } namespace { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index d30076237..d6e0fbf69 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -574,7 +574,7 @@ std::set ValidateShaders(std::span shaders, const auto expected = state.rectList ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex; Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); - for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero"); + for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance); if (i != 0) { for (const auto& [location, signature] : current.inputs) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 39c8dd8b2..29e5122ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4388,7 +4388,7 @@ void validationTests() { { auto vertex = pushed({.push = true, .bufferArray = 2}); vertex.bindings.front().descriptorSet = 1; - expectFailure([&] { validate(vertex, 8); }, "descriptor set other than zero"); + expectFailure([&] { validate(vertex, 8); }, "descriptor set other than its stage's"); } { auto vertex = pushed({.push = true, .bufferArray = 2}); diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 268265461..3f58c1398 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -103,7 +103,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin if (info.buffers.size() > RuntimeAbi::BufferCapacity || info.images.size() > RuntimeAbi::ImageCapacity || info.samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader binding layout exceeds runtime metadata capacity"); if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); - if (registrationModules && (program.Resources().stage == IrShaderStage::Pixel || std::ranges::any_of(info.samplers, [](const auto& sampler) { return FoldsTexelOffsets(sampler); }))) next.runtimeStateDwords = RuntimeAbi::StateDwords; + if (registrationModules && program.Resources().stage == IrShaderStage::Pixel) next.runtimeStateDwords = RuntimeAbi::StateDwords; const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 851d92ed5..b76a88dce 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -257,11 +257,8 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; const auto unnormalized = ProveUnnormalized(info, snapshot); - std::uint32_t unnormalizedSamplers = 0u; for (std::uint32_t index = 0; index < info.samplers.size(); ++index) { - if (!FoldsTexelOffsets(info.samplers[index])) continue; - if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); - else if (unnormalized.samplers[index]) unnormalizedSamplers |= 1u << index; + if (FoldsTexelOffsets(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); } std::vector compareStates(info.images.size()); for (std::uint32_t index = 0; index < info.images.size(); ++index) { @@ -382,7 +379,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if ((imageHeap || samplerHeap) && compact.resources.empty()) continue; PreparedDescriptorBinding entry{}; auto& physical = entry.descriptor; - physical.descriptorSet = RuntimeAbi::DescriptorSet; + physical.descriptorSet = registrationModules && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet; physical.binding = NativeBinding(stage, compact.kind); physical.count = compact.resources.empty() ? 1u : static_cast(compact.resources.size()); physical.kind = PhysicalKindFor(compact.kind); @@ -459,7 +456,6 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } if (layout.runtimeStateDwords != 0u) { plan.runtimeState.assign(layout.runtimeStateDwords, 0u); - plan.runtimeState.at(RuntimeAbi::StateUnnormalizedSamplers) = unnormalizedSamplers; for (std::uint32_t target = 0; target < exportMappings.size(); ++target) plan.runtimeState.at(RuntimeAbi::StateExportMappings + target / 4u) |= static_cast(exportMappings[target]) << (target % 4u * 8u); } std::vector samplerMasks(info.samplers.size()); diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 8d5019b93..2a303c504 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -11,6 +11,7 @@ namespace ShaderRecompiler::RuntimeAbi { inline constexpr std::uint32_t Version = 11u; inline constexpr std::uint32_t DescriptorSet = 0u; +inline constexpr std::uint32_t FragmentDescriptorSet = 1u; inline constexpr std::uint32_t StageCount = 4u; inline constexpr std::uint32_t PushConstantDwords = 32u; inline constexpr std::uint32_t FirstImageBinding = 1u; @@ -39,9 +40,8 @@ inline constexpr std::uint32_t BindlessTableSlots = 16u; inline constexpr std::uint32_t StorageMipSlots = 4u; inline constexpr std::uint32_t StorageHeapCapacity = 16u; inline constexpr std::uint32_t SamplerHeapCapacity = 32u; -inline constexpr std::uint32_t StateUnnormalizedSamplers = 0u; -inline constexpr std::uint32_t StateExportMappings = 1u; -inline constexpr std::uint32_t StateDwords = 3u; +inline constexpr std::uint32_t StateExportMappings = 0u; +inline constexpr std::uint32_t StateDwords = 2u; inline constexpr std::uint32_t ImageModeMask = 0xffu; inline constexpr std::uint32_t CompareStateShift = 9u; inline constexpr std::uint32_t CompareStateMask = 0x1fffu; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 7297e528d..d380b33fc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1494,7 +1494,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con } else if (!offsetOperand) { sample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands); } else { - const auto selector = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateUnnormalizedSamplers), ConstantU32(state, mem.sampler)), 1u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); + const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); const auto folded = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); const auto foldLabel = state.module.AllocateId(); const auto operandLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index cc7ab59a8..4227e8d83 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -391,7 +391,7 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto Define = [&](std::uint32_t type, const char* name, std::uint32_t storage = spv::StorageClassStorageBuffer) { const auto variable = state.module.DefineGlobalVariable(TypePointer(state, storage, type), storage); state.module.AddName(variable, name); - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, state.registrationModules && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet); state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, NativeBinding(stage, binding.kind)); return variable; }; From d7e0f27d6b602413d3b728ab581982d63ae6d8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 15:18:38 +0200 Subject: [PATCH 284/318] perf(agc): describe pipeline set layouts from the compiled shaders alone Co-Authored-By: Claude --- .../Graphics/include/Pipeline.hpp | 9 +++++ .../Graphics/include/ShaderResources.hpp | 9 +++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 +++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 38 +++++++++++-------- .../Graphics/src/ShaderResources.cpp | 38 +++++++++++++++++++ 5 files changed, 84 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 0b97a08f6..ee0f2d094 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -41,6 +41,7 @@ public: // before, during and after the pass (GENERAL for resident targets, which then need no // transitions). Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); ~Pipeline(); Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; @@ -98,6 +99,14 @@ private: // only evicted once no recorded draw holds them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per // draw as before. std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); +struct DescribedSetLayouts { + PipelineSetLayouts layouts; + std::vector mainKey; + std::vector fragmentKey; + PipelineSetLayouts View() const { return {layouts.main, layouts.fragment, layouts.split, mainKey, fragmentKey}; } +}; +DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders); // Destroys the cached pipelines of a device; to be called before the device goes away. Without it, // entries of a gone device are recognised by their buffer pool (made and reset with the device, so // it tells device instances apart when the loader reuses a VkDevice handle) and forgotten unused. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 4e4cf13b4..7b29f6df0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -51,6 +51,14 @@ bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddres // a storage image other than `except` has results pending in [address, address + bytes). bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except); +struct PipelineSetLayouts { + VkDescriptorSetLayout main = VK_NULL_HANDLE; + VkDescriptorSetLayout fragment = VK_NULL_HANDLE; + bool split = false; + std::span mainKey; + std::span fragmentKey; +}; + class DescriptorCache { public: explicit DescriptorCache(const Context& context); @@ -119,6 +127,7 @@ public: std::span LayoutBindingFlags() const { return layoutBindingFlags; } VkDescriptorSetLayout FragmentLayout() const { return fragment.layout; } bool SplitSets() const { return splitSets; } + PipelineSetLayouts SetLayouts() const { return {_layout, fragment.layout, splitSets, layoutKey, fragmentLayoutKey}; } void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; struct DrawBindings { DrawBindings() = default; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 914e7b269..946f2a560 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2118,6 +2118,12 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& masked->blends.at(state.colors[index].exportIndex).colorWriteMask &= VK_COLOR_COMPONENT_R_BIT; } const State& pipelineState = masked.has_value() ? *masked : state; + static const bool verifySetLayouts = std::getenv("APS5_VERIFY_SET_LAYOUTS") != nullptr; + if (verifySetLayouts && context.descriptorCache != nullptr) { + const auto described = DescribeSetLayouts(context, shaders); + const auto built = resources->SetLayouts(); + Require(described.layouts.main == built.main && described.layouts.fragment == built.fragment && described.layouts.split == built.split && std::ranges::equal(described.mainKey, built.mainKey) && std::ranges::equal(described.fragmentKey, built.fragmentKey), "set layouts described from the shaders differ from the draw's"); + } auto pipeline = CachedPipeline(context, pipelineState, inputs.vertexInput, *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); // Resident targets' views are stable while their storage image lives, so the framebuffer is // reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 2bcd134d1..aaff3c414 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -163,7 +163,7 @@ void appendKey(std::vector& key, const TValue& value) { key.insert(key.end(), bytes.begin(), bytes.end()); } -bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders) { +bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false; keys = {}; @@ -217,9 +217,9 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); - if (!resources.SplitSets()) { + if (!resources.split) { appendKey(keys.layout, pushStagesOf(context)); - for (const auto word : resources.LayoutKey()) appendKey(keys.layout, word); + for (const auto word : resources.mainKey) appendKey(keys.layout, word); keys.preRasterizationLayout = keys.fragmentLayout = keys.layout; return true; } @@ -228,12 +228,12 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(key, sets); appendKey(key, pushStagesOf(context)); if ((sets & 1u) != 0u) { - appendKey(key, resources.LayoutKey().size()); - for (const auto word : resources.LayoutKey()) appendKey(key, word); + appendKey(key, resources.mainKey.size()); + for (const auto word : resources.mainKey) appendKey(key, word); } if ((sets & 2u) != 0u) { - appendKey(key, resources.FragmentLayoutKey().size()); - for (const auto word : resources.FragmentLayoutKey()) appendKey(key, word); + appendKey(key, resources.fragmentKey.size()); + for (const auto word : resources.fragmentKey) appendKey(key, word); } }; splitKey(keys.layout, 3u); @@ -244,7 +244,9 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } -Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : Pipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout) {} + +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); sampleLocationsEnabled = ValidateSampleLocations(context, state); // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep @@ -300,12 +302,12 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } // A descriptor set layout with the same bindings as this one is compatible with the pipeline // layout, so later draws bind their own ShaderResources' set under it. - const std::array setLayouts{resources.Layout(), resources.FragmentLayout()}; + const std::array setLayouts{resources.main, resources.fragment}; const VkPushConstantRange push{pushStages, 0, PipelinePushSlotBytes}; const std::array slots{{{pushStagesOf(context), 0, PipelinePushSlotBytes}, {VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes}}}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.flags = resources.SplitSets() ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; - layoutInfo.setLayoutCount = resources.SplitSets() ? 2u : 1u; + layoutInfo.flags = resources.split ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; + layoutInfo.setLayoutCount = resources.split ? 2u : 1u; layoutInfo.pSetLayouts = setLayouts.data(); layoutInfo.pushConstantRangeCount = dynamicRendering ? static_cast(slots.size()) : pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = dynamicRendering ? slots.data() : pushStages != 0 ? &push : nullptr; @@ -696,7 +698,7 @@ void append(std::vector& key, const TValue& value) { // (the recompiler could not identify it, so nothing else may share its pipeline). The rect-list // control and evaluation stages are generated from the vertex and fragment results, which the key // already names, so they carry no id of their own. -void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { +void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { using Stage = ShaderRecompiler::ShaderStage; key.clear(); append(key, context.device); @@ -728,10 +730,10 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, attribute.format); append(key, attribute.offset); } - append(key, resources.LayoutKey().size()); - for (const auto word : resources.LayoutKey()) append(key, word); - append(key, resources.FragmentLayoutKey().size()); - for (const auto word : resources.FragmentLayoutKey()) append(key, word); + append(key, resources.mainKey.size()); + for (const auto word : resources.mainKey) append(key, word); + append(key, resources.fragmentKey.size()); + for (const auto word : resources.fragmentKey) append(key, word); append(key, state.hasColorTarget); append(key, state.rectList); append(key, state.topology); @@ -851,6 +853,10 @@ void reportPipelines(PipelineStore& store) { } std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { + return CachedPipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout); +} + +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { static const bool disabled = std::getenv("APS5_NO_PIPELINE_CACHE") != nullptr; if (disabled) return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); auto& store = Pipelines(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e223a6d45..7bee0f6a0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include #include @@ -1512,6 +1513,43 @@ void ShaderResources::buildPrepare(std::span shaders, cons } } +DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders) { + Require(context.descriptorCache != nullptr, "described set layouts need the descriptor cache"); + DescribedSetLayouts result; + std::array, 2> descriptions; + bool split = false; + bool updateAfterBind = false; + std::uint64_t storageBuffers = 0; + for (const auto& shader : shaders) { + Require(shader.program != nullptr, "missing compiled shader"); + const VkShaderStageFlags flags = VulkanStage(shader.stage); + std::uint64_t stageStorageBuffers = 0; + std::uint64_t stageResources = 0; + for (const auto& binding : shader.program->bindings) { + split |= binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; + const bool image = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; + const auto type = !image ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : binding.kind == ShaderRecompiler::DescriptorKind::StorageImage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : binding.kind == ShaderRecompiler::DescriptorKind::SampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER; + if (type != VK_DESCRIPTOR_TYPE_SAMPLER) stageResources += binding.count; + if (type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) { + stageStorageBuffers += binding.count; + storageBuffers += binding.count; + } + if (stageStorageBuffers > context.limits.maxPerStageDescriptorStorageBuffers || stageResources > context.limits.maxPerStageResources) updateAfterBind = true; + descriptions[binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet ? 1 : 0].push_back({binding.binding, type, binding.count, flags, nullptr}); + } + } + if (storageBuffers > context.limits.maxDescriptorSetStorageBuffers) updateAfterBind = true; + for (std::size_t set = 0; set < descriptions.size(); ++set) { + auto& key = set == 0 ? result.mainKey : result.fragmentKey; + for (const auto& binding : descriptions[set]) key.insert(key.end(), {binding.binding, static_cast(binding.descriptorType), binding.descriptorCount, binding.stageFlags}); + if (updateAfterBind && (set == 0 || split)) key.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + } + result.layouts.split = split; + result.layouts.main = context.descriptorCache->Layout(result.mainKey, descriptions[0], updateAfterBind); + if (split) result.layouts.fragment = context.descriptorCache->Layout(result.fragmentKey, descriptions[1], updateAfterBind); + return result; +} + void ShaderResources::buildComplete() { PerformanceTimer frameTiming("Resources.Complete"); Require(!completed, "cannot update a published descriptor heap"); From 7f64d14772400e83bc24fc1b75615133bc874271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 15:51:30 +0200 Subject: [PATCH 285/318] perf(agc): build the shader pipeline libraries when graphics stages are resolved at registration Co-Authored-By: Claude --- .../include/Driver/Draw/DrawCache.hpp | 3 ++ .../Execution/include/VulkanDevice.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 30 +++++++++++++++++-- .../Execution/src/VulkanDevice.cpp | 19 ++++++++++++ .../Graphics/include/Pipeline.hpp | 1 + .../Graphics/include/PipelineLibrary.hpp | 2 +- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 25 +++++++++++++++- .../Graphics/src/PipelineLibrary.cpp | 15 ++++++++-- .../tests/execution/PreparedGraphics.cpp | 17 +++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../include/Optimization/ResourceProgram.hpp | 1 + core/shader/recompiler/Recompiler.cpp | 12 ++++++++ 12 files changed, 120 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 06d954d3b..5fa283462 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -37,6 +37,9 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const struct PreparedGraphicsStage { std::shared_ptr snapshot; PreparedShaders::Entry entry; + ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex; + std::uint32_t pushOffset = 0; + std::shared_ptr registration; }; std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index fe9ea5117..2027874af 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -299,6 +299,7 @@ public: }; IndirectDrawSupport DrawIndirectSupport() const; void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}); + bool PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span shaders); private: VulkanDevice(const PresentationWindow* window, bool compileOnly); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 580d119af..ed72dcd0d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,8 @@ namespace AgcDriver::DriverDetail { std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) { try { - return ShaderRecompiler::PrepareShader(request, preparation); + auto handle = ShaderRecompiler::PrepareShader(request, preparation); + return handle; } catch (...) { try { if (std::getenv("APS5_DUMP_SHADERS") != nullptr) static_cast(Driver::dumpRequest(request.shader.codeAddress, request)); @@ -860,7 +862,14 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod } const auto bytes = handle->artifact->bindings.pushConstantSizeBytes; require(bytes <= room, "prepared graphics stages exceed the push constant block"); - prepared.push_back({program.snapshot, {program.codeOffset, handle}}); + std::shared_ptr registration; + if (target.registrationModules) { + try { + registration = ShaderRecompiler::RegistrationResult(request, *handle); + } catch (const std::exception&) { + } + } + prepared.push_back({program.snapshot, {program.codeOffset, handle}, program.binary.stage, offset, std::move(registration)}); pushOffset += bytes; } transaction.Commit(); @@ -930,6 +939,23 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std if (stage.snapshot->type == 1) continue; ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target()); } + if (localDevice->Target().registrationModules && !prepared.empty() && std::ranges::all_of(prepared, [](const auto& stage) { return stage.registration != nullptr && (stage.stage == ShaderRecompiler::ShaderStage::Vertex || stage.stage == ShaderRecompiler::ShaderStage::Fragment); }) && prepared.back().stage == ShaderRecompiler::ShaderStage::Fragment) { + std::vector> results; + std::vector> stages; + for (const auto& stage : prepared) { + results.push_back(stage.registration); + stages.emplace_back(stage.stage, stage.pushOffset); + } + SubmitRegistrationPreparation([localDevice, results = std::move(results), stages = std::move(stages), shaderStages = decoded.state.stages] { + std::vector shaders; + for (std::size_t index = 0; index < results.size(); ++index) shaders.push_back({stages[index].first, results[index].get(), stages[index].second}); + try { + static_cast(localDevice->PrebuildGraphicsLibraries(shaderStages, shaders)); + } catch (const std::exception& error) { + APS5_LOG_OUT("graphics library prebuild skipped: %s", error.what()); + } + }); + } PreparedShaderState::GraphicsAbi abi{key, registry, {}}; for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot); auto& graphicsAbis = transaction.Edit(*owner).graphicsAbis; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 858e855dc..78d96fe75 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3058,6 +3058,25 @@ void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) { Graphics::RunColorMetadataPass(graphicsContext(), pass); } +bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span shaders) { + PerformanceTimer timing("Vulkan.GraphicsPipelinePrebuild"); + const auto context = graphicsContext(); + if (!context.graphicsPipelineLibrary || context.descriptorCache == nullptr) return false; + Graphics::State state{}; + state.stages = stages; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.color.format = VK_FORMAT_R8G8B8A8_UNORM; + state.color.exportIndex = 0; + state.colors.push_back(state.color); + state.hasColorTarget = true; + state.blend.colorWriteMask = 15; + state.blends.push_back(state.blend); + state.renderExtent = {1, 1}; + const auto layouts = Graphics::DescribeSetLayouts(context, shaders); + return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), shaders); +} + void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipe) { for (const auto& shader : shaders) { require(shader.program != nullptr, "missing compiled shader"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index ee0f2d094..f3a48a189 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -43,6 +43,7 @@ public: Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); ~Pipeline(); + static bool Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders); Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index 4ffb5e3bc..5541197b2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -33,7 +33,7 @@ struct OptimizedPipeline { void Release() noexcept; }; -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, std::span modules = {}); +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, std::span modules = {}, bool partsOnly = false); void WaitForOptimizedPipelines(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index aaff3c414..911f1a10a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -152,6 +152,9 @@ bool ShadersWriteMemory(std::span shaders) { namespace { +thread_local bool prebuilding = false; +thread_local bool prebuilt = false; + VkShaderStageFlags pushStagesOf(const Context& context) { return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); } @@ -469,7 +472,18 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.renderPass = renderPass; if (dynamicRendering) pipelineInfo.pNext = &rendering; timing.Mark("modules_and_state"); - if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules); + if (prebuilding && !libraries) { + for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + _modules.clear(); + return; + } + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules, prebuilding); + if (prebuilding) { + for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + _modules.clear(); + prebuilt = true; + return; + } else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); if (alternateAlpha) { blend.pAttachments = secondChannels.data(); @@ -489,6 +503,15 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } } +bool Pipeline::Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders) { + struct Scope { + Scope() { prebuilding = true; prebuilt = false; } + ~Scope() { prebuilding = false; } + } scope; + const Pipeline pipeline(context, state, vertexInput, resources, shaders); + return prebuilt; +} + Pipeline::~Pipeline() { release(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index df499bb21..a6bbdb1cb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -102,10 +102,10 @@ std::span PipelineLibraryDynamicStates() { return dynamicStates; } -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, std::span modules) { +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, std::span modules, bool partsOnly) { Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states"); auto& store = Libraries(); - std::lock_guard lock(store.mutex); + std::unique_lock lock(store.mutex); auto& device = store.devices[context.device]; if (device.destroyPipeline == nullptr) { device.destroyPipeline = context.Function("vkDestroyPipeline"); @@ -193,7 +193,15 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip break; } VkPipeline handle = VK_NULL_HANDLE; - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle), "vkCreateGraphicsPipelines library"); + if (partsOnly) lock.unlock(); + const auto created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle); + if (partsOnly) lock.lock(); + Check(created, "vkCreateGraphicsPipelines library"); + if (const auto raced = cache.find(key); raced != cache.end()) { + device.destroyPipeline(context.device, handle, nullptr); + libraries.push_back(raced->second); + continue; + } cache.emplace(key, handle); if (optimize) { for (std::uint32_t stage = 0; stage < create.stageCount; ++stage) { @@ -207,6 +215,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip ++device.counters.built[part]; libraries.push_back(handle); } + if (partsOnly) return VK_NULL_HANDLE; PerformanceTimer timing("Vulkan.GraphicsPipelineLink"); VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 5d456afdf..7b4970657 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "VulkanTestDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #include "SceShaders.hpp" @@ -130,6 +131,22 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path } const auto stages = PrepareGraphicsStages(prepared, target); Require(stages.size() == (tessellation ? 4u : 2u), "graphics preparation compiled the wrong stages"); + if (!mesh && !tessellation && target.registrationModules) { + const auto prebuild = [&](const std::vector& group) { + Require(group.front().registration != nullptr && group.back().registration != nullptr, "registration modules: a prepared stage has no registration result"); + const std::array shaders{{{group.front().stage, group.front().registration.get(), group.front().pushOffset}, {group.back().stage, group.back().registration.get(), group.back().pushOffset}}}; + const auto before = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); + Require(device.PrebuildGraphicsLibraries(prepared.state.stages, shaders), "registration modules: the shader libraries were not prebuilt"); + const auto after = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); + return std::pair{after.built[1] - before.built[1] + after.built[2] - before.built[2], after.linked - before.linked}; + }; + const auto [first, firstLinks] = prebuild(stages); + Require(first == 2u && firstLinks == 0u, "registration modules: the prebuild did not build exactly the two shader libraries without linking"); + const auto again = PrepareGraphicsStages(prepared, target); + Require(again.front().registration != stages.front().registration, "registration modules: the second preparation reused the first result object"); + const auto [second, secondLinks] = prebuild(again); + Require(second == 0u && secondLinks == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries"); + } for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry); const auto& pixelArtifact = GetPreparedArtifact(*stages.back().entry.handle); Require(!pixelArtifact.fragmentParameters.empty() && pixelArtifact.fragmentParameters.front().sourceLocation == 3u, "prepared fragment lost interpolant mapping"); diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 88aafc8fe..6d3d68e2d 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -236,6 +236,7 @@ function(add_shader_recompiler target runtimeTarget) MatchesPreparedShader GetPreparedArtifact MaterializeShader + RegistrationResult ResolveSource ResourceMaterializer::SpecializationNanoseconds RuntimeAbi::RequireVersion diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp index f23d1b8f6..909f3cea1 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp @@ -70,6 +70,7 @@ private: void BuildPreparedShaderKey(const RecompileRequest& request, std::vector& key); [[nodiscard]] bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& handle, std::span key); [[nodiscard]] const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle); +[[nodiscard]] std::shared_ptr RegistrationResult(const RecompileRequest& request, const SourceHandle& handle); [[nodiscard]] std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle); [[nodiscard]] std::shared_ptr ResolveSource(const RecompileRequest& request); [[nodiscard]] std::shared_ptr CaptureResources(const RecompileRequest& request, const SrtRuntime& runtime, const SourceHandle& handle); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index f7d2ff5b2..2b7f9cf59 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1216,6 +1216,18 @@ const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle) { return handle.artifact->artifact; } +std::shared_ptr RegistrationResult(const RecompileRequest& request, const SourceHandle& handle) { + if (!request.target.registrationModules) throw std::runtime_error("ShaderRecompiler: registration results need registration modules"); + if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); + const auto& info = handle.artifact->info.info; + ResourceSnapshot snapshot; + snapshot.buffers.assign(info.buffers.size(), {{}, 4u}); + snapshot.images.assign(info.images.size(), {{}, 8u}); + snapshot.samplers.assign(info.samplers.size(), {{}, 4u}); + snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u); + return std::make_shared(materializeResult(*handle.artifact, request, snapshot)); +} + std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle) { if (!MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); if (capture.source != handle.source || capture.plan != handle.source->plan) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader"); From e9def903fdba1f9384ae3896ef531b20e2a89167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 16:10:39 +0200 Subject: [PATCH 286/318] perf(agc): prebuild each stage's library alone and keep two descriptor sets with pipeline libraries Co-Authored-By: Claude --- .../src/Driver/Shaders/ShaderRegistry.cpp | 46 ++++++++++++------- .../Execution/src/VulkanDevice.cpp | 23 +++++++++- .../Graphics/include/Pipeline.hpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 3 +- .../Graphics/src/ShaderResources.cpp | 19 +++++--- .../tests/execution/PreparedGraphics.cpp | 24 ++++++---- 6 files changed, 82 insertions(+), 34 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index ed72dcd0d..03e3fdd3a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -573,6 +573,7 @@ struct RegisteredPreparation { std::array memory{}; std::optional request; std::vector abiKey; + std::shared_ptr prebuildDevice; }; std::unique_ptr PlanRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration, bool* deferred = nullptr) { @@ -671,6 +672,18 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap return plan; } +void PrebuildStageLibrary(const std::shared_ptr& device, const Graphics::ShaderStages& stages, ShaderRecompiler::ShaderStage stage, std::shared_ptr result, std::uint32_t pushOffset) { + if (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment) return; + SubmitRegistrationPreparation([device, stages, stage, result = std::move(result), pushOffset] { + const std::array shaders{{{stage, result.get(), pushOffset}}}; + try { + static_cast(device->PrebuildGraphicsLibraries(stages, shaders)); + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); + } + }); +} + std::vector PrepareRegistered(RegisteredPreparation& plan) { auto& request = *plan.request; std::vector entries; @@ -680,6 +693,12 @@ std::vector PrepareRegistered(RegisteredPreparation& pla entries.push_back({plan.codeOffset, PrepareShaderWithDiagnostics(request, &preparation)}); }; append(); + if (plan.prebuildDevice != nullptr && request.target.registrationModules && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment) { + try { + PrebuildStageLibrary(plan.prebuildDevice, {Graphics::ShaderPath::Vertex, 0u, 32u, request.context.waveSize, {}, {}}, request.shader.stage, ShaderRecompiler::RegistrationResult(request, *entries.front().handle), request.layout.pushConstantOffsetBytes); + } catch (const std::exception&) { + } + } if (request.context.compute) { request.context.compute->partialThreads = {1, 1, 1}; append(); @@ -939,22 +958,10 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std if (stage.snapshot->type == 1) continue; ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target()); } - if (localDevice->Target().registrationModules && !prepared.empty() && std::ranges::all_of(prepared, [](const auto& stage) { return stage.registration != nullptr && (stage.stage == ShaderRecompiler::ShaderStage::Vertex || stage.stage == ShaderRecompiler::ShaderStage::Fragment); }) && prepared.back().stage == ShaderRecompiler::ShaderStage::Fragment) { - std::vector> results; - std::vector> stages; + if (decoded.state.stages.path == Graphics::ShaderPath::Vertex) { for (const auto& stage : prepared) { - results.push_back(stage.registration); - stages.emplace_back(stage.stage, stage.pushOffset); + if (stage.registration != nullptr) PrebuildStageLibrary(localDevice, decoded.state.stages, stage.stage, stage.registration, stage.pushOffset); } - SubmitRegistrationPreparation([localDevice, results = std::move(results), stages = std::move(stages), shaderStages = decoded.state.stages] { - std::vector shaders; - for (std::size_t index = 0; index < results.size(); ++index) shaders.push_back({stages[index].first, results[index].get(), stages[index].second}); - try { - static_cast(localDevice->PrebuildGraphicsLibraries(shaderStages, shaders)); - } catch (const std::exception& error) { - APS5_LOG_OUT("graphics library prebuild skipped: %s", error.what()); - } - }); } PreparedShaderState::GraphicsAbi abi{key, registry, {}}; for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot); @@ -996,14 +1003,20 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span, RegisteredAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); + const auto prepare = [&] { + const auto plan = PlanRegistered(*snapshot, *localDevice, state, false); + if (plan == nullptr) return std::vector{}; + plan->prebuildDevice = localDevice; + return PrepareRegistered(*plan); + }; const bool committedAbi = CommittedAbi(*snapshot, key); - auto entries = committedAbi ? std::vector{} : PrepareRegistered(*snapshot, *localDevice, state, false); + auto entries = committedAbi ? std::vector{} : prepare(); const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis; if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) { transaction.Commit(); return; } - if (committedAbi) entries = PrepareRegistered(*snapshot, *localDevice, state, false); + if (committedAbi) entries = prepare(); auto& prepared = transaction.Edit(*snapshot); for (auto& entry : entries) { const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == entry.handle->artifact; }); @@ -1190,6 +1203,7 @@ void Driver::RegisterShader(const Shader* shader) { std::shared_ptr preparation; if (AsyncRegistrationPrepare()) { preparation = PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred); + if (preparation != nullptr) preparation->prebuildDevice = localDevice; if (preparation != nullptr && (snapshot.type == 0 || snapshot.type == 1)) BuildRegisteredAbiKey(registered, *localDevice, preparation->abiKey); snapshot.prepared->pending = preparation != nullptr; } else { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 78d96fe75..d77cdb3b9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3073,8 +3073,27 @@ bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stage state.blend.colorWriteMask = 15; state.blends.push_back(state.blend); state.renderExtent = {1, 1}; - const auto layouts = Graphics::DescribeSetLayouts(context, shaders); - return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), shaders); + std::vector group(shaders.begin(), shaders.end()); + if (group.size() == 1u) { + const bool vertex = group.front().stage == ShaderRecompiler::ShaderStage::Fragment; + static const auto stub = [](bool vertexStub) { + auto result = std::make_shared(); + const std::uint32_t model = vertexStub ? 0u : 4u; + std::vector words{0x07230203u, 0x00010000u, 0u, 6u, 0u, 0x00020011u, 1u, 0x0003000eu, 0u, 1u, 0x0005000fu, model, 4u, 0x6e69616du, 0u}; + if (!vertexStub) words.insert(words.end(), {0x00030010u, 4u, 7u}); + words.insert(words.end(), {0x00020013u, 2u, 0x00030021u, 3u, 2u, 0x00050036u, 2u, 4u, 0u, 3u, 0x000200f8u, 5u, 0x000100fdu, 0x00010038u}); + result->spirv = std::move(words); + result->variantId = vertexStub ? ~std::uint64_t{0} - 1u : ~std::uint64_t{0} - 2u; + result->runtimeAbiVersion = ShaderRecompiler::RuntimeAbi::Version; + return result; + }; + static const auto vertexStub = stub(true); + static const auto fragmentStub = stub(false); + if (vertex) group.insert(group.begin(), {ShaderRecompiler::ShaderStage::Vertex, vertexStub.get(), 0u}); + else group.push_back({ShaderRecompiler::ShaderStage::Fragment, fragmentStub.get(), Graphics::PipelinePushSlotBytes}); + } + const auto layouts = Graphics::DescribeSetLayouts(context, group); + return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), group); } void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipe) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index f3a48a189..6e6ca7fdf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -108,6 +108,7 @@ struct DescribedSetLayouts { PipelineSetLayouts View() const { return {layouts.main, layouts.fragment, layouts.split, mainKey, fragmentKey}; } }; DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders); +bool SplitsSets(const Context& context, std::span shaders); // Destroys the cached pipelines of a device; to be called before the device goes away. Without it, // entries of a gone device are recognised by their buffer pool (made and reset with the device, so // it tells device instances apart when the loader reuses a VkDevice handle) and forgotten unused. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 911f1a10a..af0b4e40b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -477,13 +477,14 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput _modules.clear(); return; } - if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules, prebuilding); if (prebuilding) { + static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules, true)); for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); prebuilt = true; return; } + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules); else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); if (alternateAlpha) { blend.pAttachments = secondChannels.data(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 7bee0f6a0..2214d3c90 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1425,7 +1425,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (drawBuild && !usesBda && std::all_of(allocations.begin(), allocations.end(), [&](const Allocation& allocation) { return !allocation.guest || allocation.pushByte >= 0 || (allocation.dataAllocation >= 0 && allocation.dataByte < allocations[static_cast(allocation.dataAllocation)].size); })) guestMemory.AllowAdjustedRegions(); guestMemory.UploadPrepare(usesBda); timing.uploadMs = phase(BuildPhase::Upload); - splitSets = std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const auto& binding) { return binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; }); }); + splitSets = SplitsSets(context, shaders); std::array, 2> descriptions; for (const auto& binding : bindings) { const auto set = setOf(binding); @@ -1449,7 +1449,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons _layout = createLayout(layoutKey, descriptions[0], layoutBindingFlags, ownsLayout); if (splitSets) { fragment.layout = createLayout(fragmentLayoutKey, descriptions[1], fragmentBindingFlags, fragment.ownsLayout); - if (!descriptions[1].empty()) { + { const auto sizes = PoolSizes(descriptions[1]); if (context.descriptorCache != nullptr && !noPoolCache) { fragment.cacheAllocation = context.descriptorCache->Allocate(fragment.layout, sizes, updateAfterBind); @@ -1465,7 +1465,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons } } } - if (splitSets && !descriptions[0].empty()) { + if (splitSets) { const auto sizes = PoolSizes(descriptions[0]); if (context.descriptorCache != nullptr && !noPoolCache) { cacheAllocation = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind); @@ -1513,11 +1513,17 @@ void ShaderResources::buildPrepare(std::span shaders, cons } } +bool SplitsSets(const Context& context, std::span shaders) { + const auto fragmentSet = [](const CompiledShader& shader) { return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const auto& binding) { return binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; }); }; + const auto fragmentStage = [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Fragment; }; + return std::any_of(shaders.begin(), shaders.end(), fragmentSet) || (context.graphicsPipelineLibrary && std::any_of(shaders.begin(), shaders.end(), fragmentStage)); +} + DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders) { Require(context.descriptorCache != nullptr, "described set layouts need the descriptor cache"); DescribedSetLayouts result; std::array, 2> descriptions; - bool split = false; + const bool split = SplitsSets(context, shaders); bool updateAfterBind = false; std::uint64_t storageBuffers = 0; for (const auto& shader : shaders) { @@ -1526,7 +1532,6 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::spanbindings) { - split |= binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; const bool image = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; const auto type = !image ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : binding.kind == ShaderRecompiler::DescriptorKind::StorageImage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : binding.kind == ShaderRecompiler::DescriptorKind::SampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER; if (type != VK_DESCRIPTOR_TYPE_SAMPLER) stageResources += binding.count; @@ -3558,11 +3563,11 @@ std::shared_ptr ShaderResources::PrepareDrawBindi std::array, 2> descriptions; for (const auto& binding : bindings) descriptions[setOf(binding)].push_back(binding.layout); result->cache = context.descriptorCache; - if (!descriptions[0].empty()) { + if (!descriptions[0].empty() || splitSets) { result->allocation = result->cache->Allocate(_layout, PoolSizes(descriptions[0])); Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); } - if (!descriptions[1].empty()) { + if (!descriptions[1].empty() || splitSets) { result->fragmentAllocation = result->cache->Allocate(fragment.layout, PoolSizes(descriptions[1])); Require(result->fragmentAllocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 7b4970657..e405f2c16 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -132,20 +132,28 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path const auto stages = PrepareGraphicsStages(prepared, target); Require(stages.size() == (tessellation ? 4u : 2u), "graphics preparation compiled the wrong stages"); if (!mesh && !tessellation && target.registrationModules) { - const auto prebuild = [&](const std::vector& group) { - Require(group.front().registration != nullptr && group.back().registration != nullptr, "registration modules: a prepared stage has no registration result"); - const std::array shaders{{{group.front().stage, group.front().registration.get(), group.front().pushOffset}, {group.back().stage, group.back().registration.get(), group.back().pushOffset}}}; + const auto prebuild = [&](const std::vector& group, std::initializer_list picked) { + std::vector shaders; + for (const auto index : picked) { + Require(group.at(index).registration != nullptr, "registration modules: a prepared stage has no registration result"); + shaders.push_back({group[index].stage, group[index].registration.get(), group[index].pushOffset}); + } const auto before = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); Require(device.PrebuildGraphicsLibraries(prepared.state.stages, shaders), "registration modules: the shader libraries were not prebuilt"); const auto after = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); - return std::pair{after.built[1] - before.built[1] + after.built[2] - before.built[2], after.linked - before.linked}; + Require(after.linked == before.linked, "registration modules: a prebuild linked a pipeline"); + return std::pair{after.built[1] - before.built[1], after.built[2] - before.built[2]}; }; - const auto [first, firstLinks] = prebuild(stages); - Require(first == 2u && firstLinks == 0u, "registration modules: the prebuild did not build exactly the two shader libraries without linking"); + const auto vertexOnly = prebuild(stages, {0u}); + Require(vertexOnly.first == 1u, "registration modules: the vertex stage alone did not build its pre-rasterization library"); + const auto pixelOnly = prebuild(stages, {1u}); + Require(pixelOnly.second == 1u, "registration modules: the pixel stage alone did not build its fragment library"); + const auto pair = prebuild(stages, {0u, 1u}); + Require(pair.first == 0u && pair.second == 0u, "registration modules: the pair of stages did not find the libraries prebuilt one stage at a time"); const auto again = PrepareGraphicsStages(prepared, target); Require(again.front().registration != stages.front().registration, "registration modules: the second preparation reused the first result object"); - const auto [second, secondLinks] = prebuild(again); - Require(second == 0u && secondLinks == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries"); + const auto repeated = prebuild(again, {0u, 1u}); + Require(repeated.first == 0u && repeated.second == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries"); } for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry); const auto& pixelArtifact = GetPreparedArtifact(*stages.back().entry.handle); From 67556722e1bd63408cdab8b6a15eb85f2f5c83da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 16:23:59 +0200 Subject: [PATCH 287/318] perf(recompiler): read vertex attribute component selectors at run time when modules are built at registration Co-Authored-By: Claude --- .../tests/execution/ZeroStrideVertexFetch.cpp | 17 ++++++++++--- .../Optimization/src/BindingAllocator.cpp | 9 +++++++ core/shader/recompiler/Recompiler.cpp | 25 ++++++++++++++++--- core/shader/recompiler/RuntimeAbi.hpp | 2 ++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp index 50e546f07..a308a4063 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp @@ -37,11 +37,14 @@ struct Case { std::uint32_t records; bool inRange; const char* what; + std::uint32_t selectors = 0xfacu; }; +std::vector registrationModule; + std::array VertexBufferDescriptor(const Case& value) { const auto address = reinterpret_cast(Element.data()); - return {static_cast(address), static_cast((address >> 32u) & 0xffffu), value.records, 0x0104dfacu | (value.select << 28u)}; + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), value.records, (0x0104dfacu & ~0xfffu) | value.selectors | (value.select << 28u)}; } std::array OutputDescriptor() { @@ -74,6 +77,10 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) { vertexRequest.useCache = false; const auto vertexResult = ShaderRecompiler::Recompile(vertexRequest); Require(vertexResult.vertexAttributes.size() == 1u, std::string(value.what) + ": the fetch did not become a vertex attribute"); + if (device.Target().registrationModules) { + if (registrationModule.empty()) registrationModule = vertexResult.spirv.Words(); + Require(vertexResult.spirv.Words() == registrationModule, std::string(value.what) + ": registration modules: the V# specialized the vertex module"); + } const auto vertexPush = static_cast(vertexResult.pushConstants.size()); ShaderRecompiler::ShaderPixelStageInfo pixel{}; @@ -120,7 +127,9 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) { void Check(const Case& value) { for (std::uint32_t vertex = 0; vertex < Vertices; ++vertex) { for (std::uint32_t component = 0; component < 4u; ++component) { - const auto expected = value.inRange ? std::bit_cast(Element[component]) : 0u; + const auto selector = (value.selectors >> (component * 3u)) & 7u; + const auto selected = selector >= 4u ? std::bit_cast(Element[selector - 4u]) : selector == 1u ? std::bit_cast(1.0f) : 0u; + const auto expected = value.inRange || selector < 4u ? selected : 0u; const auto actual = Output[vertex * 4u + component]; Require(actual == expected, std::string(value.what) + ": vertex " + std::to_string(vertex) + " v" + std::to_string(8u + component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); } @@ -133,12 +142,14 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - constexpr std::array cases{{ + constexpr std::array cases{{ {2u, 1u, true, "OOB_SELECT 2, one record"}, {2u, 0u, false, "OOB_SELECT 2, no records"}, {3u, 16u, true, "OOB_SELECT 3, whole element in range"}, {3u, 0u, false, "OOB_SELECT 3, no records"}, {0u, 16u, true, "OOB_SELECT 0, whole element in range"}, + {3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, + {3u, 0u, false, "OOB_SELECT 3, no records, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, }}; for (const auto& value : cases) { Draw(*device, value); diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 3f58c1398..ceaca33ff 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -104,6 +104,15 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); if (registrationModules && program.Resources().stage == IrShaderStage::Pixel) next.runtimeStateDwords = RuntimeAbi::StateDwords; + if (registrationModules && program.Resources().stage != IrShaderStage::Pixel) { + std::uint32_t attributes = 0u; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetAttribute && inst->Flags() == 1u) attributes = std::max(attributes, inst->Argument(0)->Resolve()->ImmediateU32() + 1u); + } + } + next.runtimeStateDwords = (attributes + RuntimeAbi::VertexSelectorsPerDword - 1u) / RuntimeAbi::VertexSelectorsPerDword; + } const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 2b7f9cf59..e09f87430 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1,5 +1,6 @@ #include "Recompiler.hpp" #include +#include #include #include #include @@ -714,7 +715,7 @@ std::shared_ptr specializeModule(const CompiledShaderAr return entry->module; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot) { +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults = false) { RecompileResult result; const auto& artifact = variant.artifact; static_cast(result) = { @@ -751,6 +752,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; + std::array runtimeSelectors{}; for (const auto& input : result.vertexInputs) { if (!request.context.vertex || input.location >= request.context.vertex->resourcesNum || input.location >= request.context.vertex->resources.size()) throw std::runtime_error("Shader cache: invalid vertex attribute metadata"); const auto& vertex = *request.context.vertex; @@ -758,7 +760,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto fetchIndex = vertex.fetchEmbedded ? vertex.resourcesDst[input.location].fetchIndex : input.fetchIndex; result.vertexAttributes.push_back({input.location, input.components, resource, fetchIndex}); if (!artifact.vertexInputPatches.empty()) { - const auto numeric = VertexInputNumericClass(static_cast((resource.fields[3] >> 12u) & 0x7fu)); + const auto format = static_cast((resource.fields[3] >> 12u) & 0x7fu); + const auto numeric = registrationDefaults && format == IrBufferFormat::Invalid ? IrTextureNumericClass::Float : VertexInputNumericClass(format); if (numeric == IrTextureNumericClass::Unsupported) throw std::runtime_error("unsupported prepared vertex format"); const auto kind = numeric == IrTextureNumericClass::Float ? 0u : numeric == IrTextureNumericClass::Sint ? 1u : 2u; vertexClasses.at(input.location) = kind; @@ -771,7 +774,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (selector >= 4u) formatComponents = std::max(formatComponents, selector - 3u); selectors |= selector << (component * 3u); } - moduleKey.push_back(selectors); + moduleKey.push_back(request.target.registrationModules ? selectors & ~0xfffu : selectors); + if (request.target.registrationModules) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u); } } if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel) { @@ -828,6 +832,19 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info); result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); + const auto& layout = variant.bindings.layout; + if (request.target.registrationModules && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) { + const auto first = layout.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors; + if (layout.runtimeStateDwords > runtimeSelectors.size()) throw std::runtime_error("vertex selector state exceeds its capacity"); + if (layout.UsesPushData()) { + if ((first + layout.runtimeStateDwords) * sizeof(std::uint32_t) > result.pushConstants.size()) throw std::runtime_error("vertex selector state lies outside the push data"); + std::memcpy(result.pushConstants.data() + first * sizeof(std::uint32_t), runtimeSelectors.data(), layout.runtimeStateDwords * sizeof(std::uint32_t)); + } else { + const auto data = std::ranges::find_if(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::ShaderData; }); + if (data == result.bindings.end() || first + layout.runtimeStateDwords > data->guestDescriptor.size()) throw std::runtime_error("vertex selector state lies outside the shader data"); + std::copy_n(runtimeSelectors.begin(), layout.runtimeStateDwords, data->guestDescriptor.begin() + first); + } + } result.poisonedSrtReads = static_cast(snapshot.srtPoison.size()) + snapshot.nullRootReads; return result; } @@ -1225,7 +1242,7 @@ std::shared_ptr RegistrationResult(const RecompileRequest snapshot.images.assign(info.images.size(), {{}, 8u}); snapshot.samplers.assign(info.samplers.size(), {{}, 4u}); snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u); - return std::make_shared(materializeResult(*handle.artifact, request, snapshot)); + return std::make_shared(materializeResult(*handle.artifact, request, snapshot, true)); } std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle) { diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 2a303c504..eb5ce8cfc 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -42,6 +42,8 @@ inline constexpr std::uint32_t StorageHeapCapacity = 16u; inline constexpr std::uint32_t SamplerHeapCapacity = 32u; inline constexpr std::uint32_t StateExportMappings = 0u; inline constexpr std::uint32_t StateDwords = 2u; +inline constexpr std::uint32_t StateVertexSelectors = 0u; +inline constexpr std::uint32_t VertexSelectorsPerDword = 2u; inline constexpr std::uint32_t ImageModeMask = 0xffu; inline constexpr std::uint32_t CompareStateShift = 9u; inline constexpr std::uint32_t CompareStateMask = 0x1fffu; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index ac2cc1366..f20d094a8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1250,7 +1250,7 @@ std::uint32_t EmitGetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) const auto component = inst.Argument(1)->ImmediateU32(); if (state.inputInfo.vertex == nullptr || !state.inputInfo.vertex->fetchEmbedded || component >= 4u) ctx.Fail(inst, "invalid prepared vertex attribute"); const auto first = PipelineSpecialization::VertexBase + attribute * PipelineSpecialization::VertexWords; - const auto selector = state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u); + const auto selector = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors + attribute / RuntimeAbi::VertexSelectorsPerDword), ConstantU32(state, attribute % RuntimeAbi::VertexSelectorsPerDword * 16u + component * 3u)), 7u) : state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u); const auto one = state.module.SpecializationConstant(TypeU32(state), first + 4u, 0x3f800000u); auto value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, 1u)), one, ConstantU32(state, 0u)); for (std::uint32_t channel = 0; channel < 4u; ++channel) value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, channel + 4u)), EmitAttributeValue(state, attribute, channel), value); From 9eb42ca2d24523b424fbbd9ec304f0d280575754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 00:02:12 +0200 Subject: [PATCH 288/318] fix(agc): destroy pipeline library shader modules once the library is built Co-Authored-By: Claude --- .../Graphics/include/PipelineLibrary.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 ++-- .../Graphics/src/PipelineLibrary.cpp | 15 +-------------- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index 5541197b2..d121692b4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -33,7 +33,7 @@ struct OptimizedPipeline { void Release() noexcept; }; -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, std::span modules = {}, bool partsOnly = false); +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, bool partsOnly = false); void WaitForOptimizedPipelines(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index af0b4e40b..31cd07f34 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -478,13 +478,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput return; } if (prebuilding) { - static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules, true)); + static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, true)); for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); prebuilt = true; return; } - if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules); + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized); else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); if (alternateAlpha) { blend.pAttachments = secondChannels.data(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index a6bbdb1cb..7f8ebfca3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -38,10 +38,8 @@ using Key = std::vector; struct DeviceLibraries { PFN_vkDestroyPipeline destroyPipeline = nullptr; PFN_vkDestroyPipelineLayout destroyLayout = nullptr; - PFN_vkDestroyShaderModule destroyModule = nullptr; std::map layouts; std::array, 4> parts; - std::vector modules; PipelineLibraryCounters counters; }; @@ -102,7 +100,7 @@ std::span PipelineLibraryDynamicStates() { return dynamicStates; } -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, std::span modules, bool partsOnly) { +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, bool partsOnly) { Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states"); auto& store = Libraries(); std::unique_lock lock(store.mutex); @@ -110,7 +108,6 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip if (device.destroyPipeline == nullptr) { device.destroyPipeline = context.Function("vkDestroyPipeline"); device.destroyLayout = context.Function("vkDestroyPipelineLayout"); - device.destroyModule = context.Function("vkDestroyShaderModule"); } const auto layoutFor = [&](const Key& key, std::optional set) { auto found = device.layouts.find(key); @@ -203,15 +200,6 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip continue; } cache.emplace(key, handle); - if (optimize) { - for (std::uint32_t stage = 0; stage < create.stageCount; ++stage) { - for (auto& module : modules) { - if (module == VK_NULL_HANDLE || module != create.pStages[stage].module) continue; - device.modules.push_back(module); - module = VK_NULL_HANDLE; - } - } - } ++device.counters.built[part]; libraries.push_back(handle); } @@ -275,7 +263,6 @@ void ClearPipelineLibraries(VkDevice device) { for (auto& part : libraries.parts) { for (const auto& [key, handle] : part) libraries.destroyPipeline(device, handle, nullptr); } - for (const auto module : libraries.modules) libraries.destroyModule(device, module, nullptr); for (const auto& [key, handle] : libraries.layouts) libraries.destroyLayout(device, handle, nullptr); store.devices.erase(found); } From a326fa84f7595afc9cf63480bbdf18b58c175ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 00:02:12 +0200 Subject: [PATCH 289/318] fix(recompiler): give registration results a flattened SRT placeholder Co-Authored-By: Claude --- core/shader/recompiler/Recompiler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index e09f87430..df88fc11a 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1242,6 +1242,7 @@ std::shared_ptr RegistrationResult(const RecompileRequest snapshot.images.assign(info.images.size(), {{}, 8u}); snapshot.samplers.assign(info.samplers.size(), {{}, 4u}); snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u); + snapshot.flattenedSrt.assign(1u, 0u); return std::make_shared(materializeResult(*handle.artifact, request, snapshot, true)); } From 361c7e2024baa92d1b93265add7c692730c10b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 01:39:28 +0200 Subject: [PATCH 290/318] perf(agc): keep oversized shaders specialized and prebuild libraries on their own thread Co-Authored-By: Claude --- .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 28 +++++++++++++----- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 ++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 4 ++- .../tests/execution/VertexFetch.cpp | 5 +++- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 2 ++ .../Optimization/DescriptorBindingBuilder.hpp | 2 +- .../src/DescriptorBindingBuilder.cpp | 4 +-- core/shader/recompiler/Recompiler.cpp | 29 +++++++++++++------ core/shader/recompiler/Recompiler.hpp | 2 ++ core/shader/recompiler/ShaderDiskCache.cpp | 6 ++-- .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- 16 files changed, 67 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index f0ee74af4..ad90c2db7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -103,6 +103,7 @@ std::shared_ptr RegisteredHeader(const ShaderRegistry& reg std::shared_ptr RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list types); void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot); void SubmitRegistrationPreparation(std::function task); +void SubmitLibraryPrebuild(std::function task); void CloseRegistrationPreparation(); void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 03e3fdd3a..502cf603c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -673,8 +673,8 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap } void PrebuildStageLibrary(const std::shared_ptr& device, const Graphics::ShaderStages& stages, ShaderRecompiler::ShaderStage stage, std::shared_ptr result, std::uint32_t pushOffset) { - if (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment) return; - SubmitRegistrationPreparation([device, stages, stage, result = std::move(result), pushOffset] { + if (result == nullptr || (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment)) return; + SubmitLibraryPrebuild([device, stages, stage, result = std::move(result), pushOffset] { const std::array shaders{{{stage, result.get(), pushOffset}}}; try { static_cast(device->PrebuildGraphicsLibraries(stages, shaders)); @@ -696,7 +696,8 @@ std::vector PrepareRegistered(RegisteredPreparation& pla if (plan.prebuildDevice != nullptr && request.target.registrationModules && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment) { try { PrebuildStageLibrary(plan.prebuildDevice, {Graphics::ShaderPath::Vertex, 0u, 32u, request.context.waveSize, {}, {}}, request.shader.stage, ShaderRecompiler::RegistrationResult(request, *entries.front().handle), request.layout.pushConstantOffsetBytes); - } catch (const std::exception&) { + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); } } if (request.context.compute) { @@ -717,7 +718,15 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap class RegistrationPreparePool { public: static RegistrationPreparePool& Instance() { - static auto* pool = new RegistrationPreparePool(); + static auto* pool = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u)); + return *pool; + } + + static RegistrationPreparePool& Prebuild() { + static auto* pool = new RegistrationPreparePool([] { + const char* text = std::getenv("APS5_PREBUILD_THREADS"); + return text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u; + }()); return *pool; } @@ -741,8 +750,7 @@ public: } private: - RegistrationPreparePool() { - const auto count = std::max(2u, std::thread::hardware_concurrency() / 2u); + explicit RegistrationPreparePool(unsigned count) { pthread_attr_t attributes; pthread_attr_init(&attributes); pthread_attr_setstacksize(&attributes, 64u << 20u); @@ -823,8 +831,13 @@ void SubmitRegistrationPreparation(std::function task) { RegistrationPreparePool::Instance().Submit(std::move(task)); } +void SubmitLibraryPrebuild(std::function task) { + RegistrationPreparePool::Prebuild().Submit(std::move(task)); +} + void CloseRegistrationPreparation() { RegistrationPreparePool::Instance().Close(); + RegistrationPreparePool::Prebuild().Close(); } std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) { @@ -885,7 +898,8 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod if (target.registrationModules) { try { registration = ShaderRecompiler::RegistrationResult(request, *handle); - } catch (const std::exception&) { + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); } } prepared.push_back({program.snapshot, {program.codeOffset, handle}, program.binary.stage, offset, std::move(registration)}); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index b0822a499..063d4a499 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -71,6 +71,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade require(left.hostSubgroupSize == right.hostSubgroupSize, prefix + "host subgroup size differs"); require(left.vertexInputs == right.vertexInputs, prefix + "vertex inputs differ"); require(left.vertexInputPatches == right.vertexInputPatches, prefix + "vertex type patches differ"); + require(left.registrationModule == right.registrationModule, prefix + "registration module flags differ"); require(left.vertexOffsetSgpr == right.vertexOffsetSgpr && left.instanceOffsetSgpr == right.instanceOffsetSgpr, prefix + "offset SGPRs differ"); require(left.vertexOffsetShared == right.vertexOffsetShared && left.instanceOffsetShared == right.instanceOffsetShared && left.vertexOffsetConflict == right.vertexOffsetConflict && left.instanceOffsetConflict == right.instanceOffsetConflict, prefix + "offset flags differ"); require(left.parameterExports == right.parameterExports, prefix + "parameter exports differ"); @@ -153,6 +154,7 @@ RecompileResult sampleResult() { result.workgroupMemoryDwords = 32769u; result.vertexInputs = {{1, 4, 2}, {5, 2, 0}}; result.vertexInputPatches = {{1, 20, {7, 8, 9}}, {5, 40, {10, 11, 12}}}; + result.registrationModule = true; result.specialization = {{512, 4}, {516, 0x3f800000u}, {517, 0}}; result.vertexAttributes = {{1, 4, {{0x1000u, 0x20000u, 0x30u, 0x4u}}, 2}, {5, 2, {{9u, 8u, 7u, 6u}}, 0}}; result.vertexInputs[0].outputMask = 5u; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index e191f6d1f..03553bad8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3220,7 +3220,9 @@ int main(int argc, char** argv) { require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float64Denormals)).request.target.float64DenormPreserve, "f64 denormal preservation was lost in serialization"); auto registrationModules = uncached; registrationModules.target.registrationModules = true; - require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(registrationModules)).request.target.registrationModules, "registration modules were lost in serialization"); + registrationModules.target.registrationWordLimit = 1234u; + const auto roundTrip = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(registrationModules)).request.target; + require(roundTrip.registrationModules && roundTrip.registrationWordLimit == 1234u, "registration modules were lost in serialization"); std::vector plainKey; std::vector registrationKey; RecompileCacheKey::Build(uncached, plainKey); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp index c42f5c35c..14df999eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp @@ -53,8 +53,9 @@ void Run(AgcDriver::VulkanDevice& device) { std::uint32_t format; bool moved; std::uint32_t offset; + bool specializedPixel = false; }; - constexpr std::array cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}}}; + constexpr std::array cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}, {16u, 77u, false, 1u, true}}}; for (const auto& value : cases) { auto& storage = value.moved ? movedVertices : vertices; storage.fill(std::byte{0}); @@ -86,7 +87,9 @@ void Run(AgcDriver::VulkanDevice& device) { pixelInfo.targetOutputMode[0] = 9u; pixelInfo.targetExportMapping.fill(0xe4u); ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; + if (value.specializedPixel) pixelRequest.target.registrationWordLimit = 1u; const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + if (device.Target().registrationModules) Require(vertex.registrationModule && pixel.registrationModule != value.specializedPixel, "registration modules: the word limit did not choose the module mode"); const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index b50278685..944d076de 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -241,6 +241,7 @@ private: append(key, value.narrowSubgroupClock); append(key, value.float64DenormPreserve); append(key, value.registrationModules); + append(key, value.registrationWordLimit); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index b2f5d2007..2bab2bb46 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -755,6 +755,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteBool(request.target.narrowSubgroupClock); writer.WriteBool(request.target.float64DenormPreserve); writer.WriteBool(request.target.registrationModules); + writer.WriteU32(request.target.registrationWordLimit); return base64Encode(buffer); } @@ -785,6 +786,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 12u) result.request.target.narrowSubgroupClock = reader.ReadBool(); if (version >= 16u) result.request.target.float64DenormPreserve = reader.ReadBool(); if (version >= 17u) result.request.target.registrationModules = reader.ReadBool(); + if (version >= 17u) result.request.target.registrationWordLimit = reader.ReadU32(); return result; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 6c15439dc..84341598a 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -35,7 +35,7 @@ struct DescriptorBindingPlan { class DescriptorBindingBuilder { public: - DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false) const; + DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false, bool fragmentDescriptorSet = false) const; DescriptorBindingPlan Select(const DescriptorBindingPlan& plan, std::span bindings) const; void Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads, std::span exportMappings = {}) const; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index b76a88dce..5277129c9 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -252,7 +252,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con allocation.specialization = std::move(plan.specialization); } -DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules) const { +DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules, bool fragmentDescriptorSet) const { if (stage == IrShaderStage::Pixel && exportMappings.size() != 8u) fail("fragment runtime export mappings are missing"); if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; @@ -379,7 +379,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if ((imageHeap || samplerHeap) && compact.resources.empty()) continue; PreparedDescriptorBinding entry{}; auto& physical = entry.descriptor; - physical.descriptorSet = registrationModules && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet; + physical.descriptorSet = fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet; physical.binding = NativeBinding(stage, compact.kind); physical.count = compact.resources.empty() ? 1u : static_cast(compact.resources.size()); physical.kind = PhysicalKindFor(compact.kind); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index df88fc11a..727334c6e 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -408,7 +408,8 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra shaderInfoCollector.Collect(program, inputInfo); constexpr BindingAllocator bindingAllocator; - auto bindings = bindingAllocator.Allocate(program, request.layout, request.target.registrationModules); + bool registration = request.target.registrationModules; + auto bindings = bindingAllocator.Allocate(program, request.layout, registration); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; @@ -420,12 +421,21 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets; targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock; targetOptions.float64DenormPreserve = request.target.float64DenormPreserve; - targetOptions.registrationModules = request.target.registrationModules; + targetOptions.registrationModules = registration; + targetOptions.fragmentDescriptorSet = request.target.registrationModules; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; result.variantId = nextVariantId(); result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + if (registration && result.spirv.size() > request.target.registrationWordLimit) { + registration = false; + program.Metadata().bindingLayoutComplete = false; + bindings = bindingAllocator.Allocate(program, request.layout, false); + targetOptions.registrationModules = false; + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + } + result.registrationModule = registration; result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u; result.memoryOffsetDword = bindings.layout.memoryOffsetDword; @@ -636,7 +646,7 @@ std::shared_ptr registrationModule(std::uint64_t varian return entry->module; } -std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool registrationModules) { +std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool fragmentDescriptorSet) { struct BindingPlanKeyStorage {}; auto& key = HostThreadLocal, BindingPlanKeyStorage>(); key.clear(); @@ -677,7 +687,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } std::call_once(plan->ready, [&] { try { - plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, registrationModules); + plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, variant.artifact.registrationModule, fragmentDescriptorSet); } catch (...) { plan->failure = std::current_exception(); } @@ -724,6 +734,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .imageMetadataDword = artifact.imageMetadataDword, .runtimeImageCount = artifact.runtimeImageCount, .runtimeImageResources = artifact.runtimeImageResources, + .registrationModule = artifact.registrationModule, .bdaAbiVersion = artifact.bdaAbiVersion, .runtimeAbiVersion = artifact.runtimeAbiVersion, .vertexInputs = artifact.vertexInputs, @@ -774,8 +785,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (selector >= 4u) formatComponents = std::max(formatComponents, selector - 3u); selectors |= selector << (component * 3u); } - moduleKey.push_back(request.target.registrationModules ? selectors & ~0xfffu : selectors); - if (request.target.registrationModules) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u); + moduleKey.push_back(artifact.registrationModule ? selectors & ~0xfffu : selectors); + if (artifact.registrationModule) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u); } } if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel) { @@ -806,7 +817,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil constants.push_back({first + 5u, kind}); } } - const auto module = request.target.registrationModules && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); + const auto module = artifact.registrationModule && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); if (variant.bindings.layout.UsesPushData() && !module->pushData) throw std::runtime_error("specialization removed the prepared push constant interface"); auto selected = DescriptorBindingBuilder{}.Select(plan->bindings, module->bindings); entry->bindings = std::move(selected); @@ -833,7 +844,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); const auto& layout = variant.bindings.layout; - if (request.target.registrationModules && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) { + if (artifact.registrationModule && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) { const auto first = layout.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors; if (layout.runtimeStateDwords > runtimeSelectors.size()) throw std::runtime_error("vertex selector state exceeds its capacity"); if (layout.UsesPushData()) { @@ -1234,8 +1245,8 @@ const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle) { } std::shared_ptr RegistrationResult(const RecompileRequest& request, const SourceHandle& handle) { - if (!request.target.registrationModules) throw std::runtime_error("ShaderRecompiler: registration results need registration modules"); if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); + if (!request.target.registrationModules || !handle.artifact->artifact.registrationModule) return nullptr; const auto& info = handle.artifact->info.info; ResourceSnapshot snapshot; snapshot.buffers.assign(info.buffers.size(), {{}, 4u}); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 731ffa1c7..5a0da6a5e 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -230,6 +230,7 @@ struct SpirvTarget { bool fixedPushSlots = false; bool float64DenormPreserve = false; bool registrationModules = false; + std::uint32_t registrationWordLimit = 200000u; }; struct BindingLayout { @@ -476,6 +477,7 @@ struct CompiledShaderArtifact { std::uint32_t imageMetadataDword = 0; std::uint32_t runtimeImageCount = 0; std::vector runtimeImageResources; + bool registrationModule = false; std::uint32_t bdaAbiVersion = 0; std::uint32_t runtimeAbiVersion = RuntimeAbi::Version; std::vector vertexInputs; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 0949aef04..24b6a9e37 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -64,9 +64,9 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 192, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 200, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 312, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 320, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 568, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -297,6 +297,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.imageMetadataDword); writer.Value(result.runtimeImageCount); writer.Values(std::span(result.runtimeImageResources)); + writer.Value(result.registrationModule); writer.List(result.vertexInputPatches, [](Writer& out, const VertexInputPatch& patch) { out.Value(patch.location); out.Value(patch.word); @@ -339,6 +340,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.imageMetadataDword); reader.Value(result.runtimeImageCount); reader.Values(result.runtimeImageResources); + reader.Value(result.registrationModule); reader.List(result.vertexInputPatches, 20, [](Reader& in, VertexInputPatch& patch) { in.Value(patch.location); in.Value(patch.word); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index dbd24615a..65cb02117 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -22,6 +22,7 @@ struct SpirvTargetOptions { bool narrowSubgroupClock = false; bool float64DenormPreserve = false; bool registrationModules = false; + bool fragmentDescriptorSet = false; }; class SpirvEmitter { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index ff712c17e..670bd401e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -92,6 +92,7 @@ struct SpirvEmitterState { bool narrowSubgroupClock = false; bool float64DenormPreserve = false; bool registrationModules = false; + bool fragmentDescriptorSet = false; std::uint32_t bufferMisalignment = 0; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index df64b366a..a3be534ad 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -251,6 +251,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.narrowSubgroupClock = target.narrowSubgroupClock; state.float64DenormPreserve = target.float64DenormPreserve; state.registrationModules = target.registrationModules; + state.fragmentDescriptorSet = target.fragmentDescriptorSet; state.hostSubgroupSize = target.subgroupSize; state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u; if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 4227e8d83..58fce16aa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -391,7 +391,7 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto Define = [&](std::uint32_t type, const char* name, std::uint32_t storage = spv::StorageClassStorageBuffer) { const auto variable = state.module.DefineGlobalVariable(TypePointer(state, storage, type), storage); state.module.AddName(variable, name); - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, state.registrationModules && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, state.fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet); state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, NativeBinding(stage, binding.kind)); return variable; }; From acc0a0487d955ee5b58674b2684a937798a640e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 01:50:10 +0200 Subject: [PATCH 291/318] docs: describe shader modules and pipeline libraries built at registration Co-Authored-By: Claude --- docs/dev/ARCHITECTURE.md | 1 + docs/dev/TechnicalDebt.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 203602f2b..4d5121846 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -91,6 +91,7 @@ flowchart LR - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. +- On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4481d2cbc..b5759c172 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -405,6 +405,8 @@ Throughout the project, every function at every stage either **does exactly what - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline +- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding and the vertex attribute numeric class stay specialized, and a pixel shader whose image needs a multisampled descriptor gets no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. +- [Prebuilt pipeline libraries](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. From d809e6d0db403947064a5a651429f7e67a4adfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 02:55:45 +0200 Subject: [PATCH 292/318] fix(agc): close the library prebuild pool only if it was created Co-Authored-By: Claude --- .../src/Driver/Shaders/ShaderRegistry.cpp | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 502cf603c..ad9c4f949 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -723,13 +723,27 @@ public: } static RegistrationPreparePool& Prebuild() { - static auto* pool = new RegistrationPreparePool([] { + static auto* pool = [] { const char* text = std::getenv("APS5_PREBUILD_THREADS"); - return text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u; - }()); + auto* created = new RegistrationPreparePool(text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u); + PrebuildSlot().store(created); + return created; + }(); return *pool; } + static RegistrationPreparePool* ExistingPrebuild() { + return PrebuildSlot().load(); + } + +private: + static std::atomic& PrebuildSlot() { + static std::atomic slot{nullptr}; + return slot; + } + +public: + void Submit(std::function task) { { std::unique_lock lock(mutex); @@ -837,7 +851,7 @@ void SubmitLibraryPrebuild(std::function task) { void CloseRegistrationPreparation() { RegistrationPreparePool::Instance().Close(); - RegistrationPreparePool::Prebuild().Close(); + if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close(); } std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) { From 672e43ca678ff8d50890b5f5154fe5095579994b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 03:06:20 +0200 Subject: [PATCH 293/318] perf(recompiler): keep compute shaders specialized; only prebuilt vertex and pixel shaders take the registration form Co-Authored-By: Claude --- .../tests/execution/BufferDescriptorRuntime.cpp | 8 +------- .../tests/execution/EmulatedColorCompare.cpp | 2 -- .../prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp | 3 +-- core/shader/recompiler/Recompiler.cpp | 2 +- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index eafac530e..45364ef78 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -72,8 +72,6 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { {"records 64 base +2", 2, 64, true}, {"absent", 0, 128, false}, }}; - const void* first = nullptr; - std::uint64_t variant = 0; for (const auto& test : cases) { Output.fill(Fill); const auto input = test.present ? BufferDescriptor(Input.data() + test.baseOffset, test.records) : std::array{0u, 0u, 0u, 0x31016facu}; @@ -84,11 +82,7 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { runtime.userData = userData; const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); const auto result = ShaderRecompiler::MaterializeShader(request, *capture, *handle); - if (first == nullptr) { - first = result->spirv.data(); - variant = result->PipelineVariantId(); - } - if (registrationModules) Require(result->spirv.data() == first && result->PipelineVariantId() == variant, "buffer descriptor runtime, " + mode + ", " + test.name + ": the V# specialized the module"); + Require(!result->registrationModule, "buffer descriptor runtime, " + mode + ", " + test.name + ": a compute shader was emitted in registration form"); device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); for (std::uint32_t lane = 0; lane < Lanes; ++lane) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 3347835d2..cc3452070 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -243,7 +243,6 @@ std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const const auto result = Compile(device, Format8888UNorm, sampler, code, useCache); if (expectedVariant != 0) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); for (const auto& binding : result.bindings) { - if (device.Target().registrationModules) break; Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "emulated comparison retained an unused sampler association"); } @@ -294,7 +293,6 @@ void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char Output.fill(-1.0f); const auto result = Compile(device, Format8888UNorm, sampler, CubeCode, false, true, true); for (const auto& binding : result.bindings) { - if (device.Target().registrationModules) break; Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "cube emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "cube emulated comparison retained an unused sampler association"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp index ba1c7bb84..77b95c736 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/TypedHeaps.cpp @@ -30,7 +30,7 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: const auto instructions = store ? std::span(storeCode) : std::span(code); ShaderRecompiler::RecompileRequest request{{ShaderStage::Compute, reinterpret_cast(instructions.data()), instructions, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, {}}, device.Target(), {0u, 0u, 0u, 128u}}; auto result = ShaderRecompiler::Recompile(request); - Require(result.runtimeImageCount == (request.target.registrationModules ? 1u : 0u), "direct image runtime metadata does not follow the module mode"); + Require(result.runtimeImageCount == 0u, "direct image allocated runtime image metadata"); Require(result.shaderDataDwords < ShaderRecompiler::RuntimeAbi::ShaderDataDwords, "shader data was not compacted"); for (const auto& binding : result.bindings) { if (binding.role == ShaderRecompiler::DescriptorRole::GuestImages) Require(binding.count == 1u, "single image expanded into unused descriptor slots"); @@ -48,7 +48,6 @@ void Run(AgcDriver::VulkanDevice& device) { device.Dispatch(shaders.back(), 1u, 1u, 1u); device.SubmitRecorded(); } - if (device.Target().registrationModules) Require(shaders[0].spirv == shaders[1].spirv && shaders[0].spirv == shaders[2].spirv, "registration modules: a present or a null image specialized the module"); shaders.clear(); device.WaitIdle(); for (std::uint32_t batch = 0u; batch < outputs.size(); ++batch) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 727334c6e..770b10811 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -408,7 +408,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra shaderInfoCollector.Collect(program, inputInfo); constexpr BindingAllocator bindingAllocator; - bool registration = request.target.registrationModules; + bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment); auto bindings = bindingAllocator.Allocate(program, request.layout, registration); SpirvTargetOptions targetOptions {}; From 68164aee2a6d27f648aeef1c144cda07ec7596bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 03:18:05 +0200 Subject: [PATCH 294/318] docs: note that compute shaders keep the specialized form Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b5759c172..1e2429348 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -405,7 +405,7 @@ Throughout the project, every function at every stage either **does exactly what - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline -- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding and the vertex attribute numeric class stay specialized, and a pixel shader whose image needs a multisampled descriptor gets no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. +- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding and the vertex attribute numeric class stay specialized, and a pixel shader whose image needs a multisampled descriptor gets no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [Prebuilt pipeline libraries](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy From 60151b66b7e7db08d9106552e570253015d250d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 03:53:11 +0200 Subject: [PATCH 295/318] perf(recompiler): keep the format word specialized for buffers accessed with the descriptor's format Co-Authored-By: Claude --- .../recompiler/Optimization/src/DescriptorBindingBuilder.cpp | 1 + core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp | 3 ++- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 5277129c9..f88bd777d 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -282,6 +282,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l auto alignment = static_cast(buffer.typedAlignment); if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast(format)).byteSize, 4u)); if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element"); + if (registrationModules && buffer.descriptorFormatted) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 1u, descriptor.dwords[3]}); if (!registrationModules) { const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 770b10811..2a214225d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1248,6 +1248,7 @@ std::shared_ptr RegistrationResult(const RecompileRequest if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); if (!request.target.registrationModules || !handle.artifact->artifact.registrationModule) return nullptr; const auto& info = handle.artifact->info.info; + if (std::ranges::any_of(info.buffers, [](const BufferResource& buffer) { return buffer.descriptorFormatted; }) || std::ranges::any_of(info.samplers, [](const SamplerResource& sampler) { return FoldsTexelOffsets(sampler); })) return nullptr; ResourceSnapshot snapshot; snapshot.buffers.assign(info.buffers.size(), {{}, 4u}); snapshot.images.assign(info.images.size(), {{}, 8u}); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 0a3a991da..d31f80c45 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -60,7 +60,8 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const bool runtime = memory.gpuDescriptor || state.registrationModules; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); - const auto word3 = runtime ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + const bool runtimeFormat = memory.gpuDescriptor || (state.registrationModules && !state.program.Info().buffers.at(memory.resource).descriptorFormatted); + const auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); const auto u32 = TypeU32(state); auto base = ConstantU32(state, 0u); if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1e2429348..a5b631e2a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -405,7 +405,7 @@ Throughout the project, every function at every stage either **does exactly what - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline -- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding and the vertex attribute numeric class stay specialized, and a pixel shader whose image needs a multisampled descriptor gets no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. +- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [Prebuilt pipeline libraries](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy From 56d5a4985dda3e7dfda3b18209f72f841a106afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 09:14:43 +0200 Subject: [PATCH 296/318] perf(recompiler): read the formatted buffer helper's V# words as specialization constants when the format is known Co-Authored-By: Claude --- .../tests/FormattedBufferOnce.cpp | 17 ++++++++++++----- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index d4ae978ef..a05fce1a4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -55,24 +55,27 @@ Counts Count(std::span words) { return counts; } -Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false) { +Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false, bool vertex = false, bool registration = false) { std::vector code; if (gpu) code.push_back(0x7e160500u); for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {0xe0000000u | (load * 4u), 0x80020000u | ((10u + load) << 8u)}); code.push_back(0xbf8c3f70u); for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {(formattedStores ? 0xe0100000u : 0xe0700000u) | (load * 4u), 0x80010000u | ((10u + load) << 8u)}); + if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u}); code.push_back(0xbf810000u); const std::span words(code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu}; RecompileRequest request{}; - request.shader = {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; request.target = Target(); + request.target.registrationModules = registration; request.layout = {0, 0, 0, 128}; request.context.waveSize = 32; request.context.userDataBaseRegister = 0; request.context.userData = userData; - request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + if (vertex) request.context.vertex = ShaderVertexStageInfo{}; + else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; request.context.memory = memory; request.useCache = false; return Count(Recompile(request).spirv.Words()); @@ -85,6 +88,11 @@ int main() { const auto none = Compile(0); const auto one = Compile(1); const auto three = Compile(3); + const auto gpuOne = Compile(1, false, true); + const auto vertexSpecialized = Compile(1, false, false, true, false); + const auto vertexRegistration = Compile(1, false, false, true, true); + Check(vertexRegistration.words < vertexSpecialized.words + vertexSpecialized.words / 2u, "formatted buffer once: in registration form a formatted load from a known descriptor kept every format's decoder (" + std::to_string(vertexRegistration.words) + " words, " + std::to_string(vertexSpecialized.words) + " specialized)"); + Check(one.words < gpuOne.words / 2u, "formatted buffer once: a formatted load from a known descriptor kept every format's decoder (" + std::to_string(one.words) + " words, a GPU-selected descriptor needs " + std::to_string(gpuOne.words) + ")"); Check(one.calls == none.calls + 1 && one.functions == none.functions + 1, "formatted buffer once: one formatted load emitted " + std::to_string(one.calls - none.calls) + " calls and " + std::to_string(one.functions - none.functions) + " functions more than none, expected one call to one shared function"); Check(three.calls == one.calls + 2 && three.functions == one.functions, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.calls - one.calls) + " calls and " + std::to_string(three.functions - one.functions) + " functions more, expected two calls and no function"); Check(three.switches == one.switches, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.switches - one.switches) + " more format switches"); @@ -94,12 +102,11 @@ int main() { Check(storeOne.functions == none.functions + 2 && storeOne.calls == none.calls + 2, "formatted buffer once: a formatted load and a formatted store to another resource emitted " + std::to_string(storeOne.functions - none.functions) + " functions and " + std::to_string(storeOne.calls - none.calls) + " calls more than none, expected one function and one call each"); Check(storeThree.functions == storeOne.functions && storeThree.calls == storeOne.calls + 4 && storeThree.switches == storeOne.switches, "formatted buffer once: two more formatted loads and stores emitted " + std::to_string(storeThree.functions - storeOne.functions) + " functions, " + std::to_string(storeThree.calls - storeOne.calls) + " calls and " + std::to_string(storeThree.switches - storeOne.switches) + " switches more, expected four calls only"); Check(three.words - one.words < one.words / 10u, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words to a " + std::to_string(one.words) + "-word module"); - const auto gpuOne = Compile(1, false, true); const auto gpuThree = Compile(3, false, true); Check(gpuThree.functions == gpuOne.functions && gpuThree.calls > gpuOne.calls, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.functions - gpuOne.functions) + " more functions, expected calls to the shared one"); Check(gpuThree.switches == gpuOne.switches, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.switches - gpuOne.switches) + " more format switches"); Check(gpuThree.words - gpuOne.words < gpuOne.words / 10u, "formatted buffer once: 2 more formatted loads from a GPU-selected descriptor added " + std::to_string(gpuThree.words - gpuOne.words) + " SPIR-V words to a " + std::to_string(gpuOne.words) + "-word module"); - std::printf("formatted buffer once tests passed (%zu words for 1 load, %zu for 3; GPU-selected %zu and %zu)\n", one.words, three.words, gpuOne.words, gpuThree.words); + std::printf("formatted buffer once tests passed (%zu words for 1 load, %zu for 3; GPU-selected %zu and %zu; vertex %zu specialized, %zu registration)\n", one.words, three.words, gpuOne.words, gpuThree.words, vertexSpecialized.words, vertexRegistration.words); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index d31f80c45..6c61c68df 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -431,8 +431,19 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr } EmitLabel(state, state.module.AllocateId()); state.memoryByteOffsets.at(element) = parameters[4]; - state.bufferMisalignment = parameters[9]; - const RuntimeDescriptor shared{ConstantU32(state, 0u), parameters[3], parameters[5], parameters[6], nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; + const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; + RuntimeDescriptor shared{}; + if (state.registrationModules) { + state.bufferMisalignment = parameters[9]; + const auto word3 = state.program.Info().buffers.at(memory.resource).descriptorFormatted ? state.module.SpecializationConstant(u32, first + 1u, 0u) : parameters[6]; + shared = {ConstantU32(state, 0u), parameters[3], parameters[5], word3, nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; + } else { + const auto word1 = state.module.SpecializationConstant(u32, first, 0u); + const auto stride = field(state, word1, 16u, 14u); + const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))); + const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], valid, parameters[1], parameters[2]}; + } const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[10] : 0u); if (store) state.module.AddFunction(spv::OpReturn); else state.module.AddFunction(spv::OpReturnValue, value); From f9b592806c54883f79a4fc962113392aef6136b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 09:18:46 +0200 Subject: [PATCH 297/318] perf(recompiler): keep the buffer base misalignment specialized in registration form Co-Authored-By: Claude --- .../tests/FormattedBufferOnce.cpp | 3 +++ .../src/DescriptorBindingBuilder.cpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 - .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 19 +++++++------------ 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index a05fce1a4..55fe8e55d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -35,6 +35,7 @@ struct Counts { std::size_t calls = 0; std::size_t functions = 0; std::size_t notInlined = 0; + std::size_t compareExchanges = 0; }; Counts Count(std::span words) { @@ -46,6 +47,7 @@ Counts Count(std::span words) { const auto opcode = words[offset] & spv::OpCodeMask; if (opcode == spv::OpSwitch) ++counts.switches; if (opcode == spv::OpFunctionCall) ++counts.calls; + if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges; if (opcode == spv::OpFunction) { ++counts.functions; if (count > 3 && (words[offset + 3] & spv::FunctionControlDontInlineMask) != 0) ++counts.notInlined; @@ -91,6 +93,7 @@ int main() { const auto gpuOne = Compile(1, false, true); const auto vertexSpecialized = Compile(1, false, false, true, false); const auto vertexRegistration = Compile(1, false, false, true, true); + Check(vertexRegistration.compareExchanges == vertexSpecialized.compareExchanges, "formatted buffer once: in registration form a store through an aligned V# kept the unaligned compare-exchange path (" + std::to_string(vertexRegistration.compareExchanges) + " compare-exchanges, " + std::to_string(vertexSpecialized.compareExchanges) + " specialized)"); Check(vertexRegistration.words < vertexSpecialized.words + vertexSpecialized.words / 2u, "formatted buffer once: in registration form a formatted load from a known descriptor kept every format's decoder (" + std::to_string(vertexRegistration.words) + " words, " + std::to_string(vertexSpecialized.words) + " specialized)"); Check(one.words < gpuOne.words / 2u, "formatted buffer once: a formatted load from a known descriptor kept every format's decoder (" + std::to_string(one.words) + " words, a GPU-selected descriptor needs " + std::to_string(gpuOne.words) + ")"); Check(one.calls == none.calls + 1 && one.functions == none.functions + 1, "formatted buffer once: one formatted load emitted " + std::to_string(one.calls - none.calls) + " calls and " + std::to_string(one.functions - none.functions) + " functions more than none, expected one call to one shared function"); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index f88bd777d..5951e7877 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -283,6 +283,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast(format)).byteSize, 4u)); if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element"); if (registrationModules && buffer.descriptorFormatted) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 1u, descriptor.dwords[3]}); + if (registrationModules) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 3u, misalignment}); if (!registrationModules) { const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 670bd401e..cf99dfc6c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -93,7 +93,6 @@ struct SpirvEmitterState { bool float64DenormPreserve = false; bool registrationModules = false; bool fragmentDescriptorSet = false; - std::uint32_t bufferMisalignment = 0; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; // The target's SPIR-V version and what the device accepts, for capabilities an emitter adds diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 6c61c68df..2218a608a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -144,8 +144,6 @@ std::uint32_t baseMisalignment(SpirvValueEmitContext& context, const IrValue& in const auto& memory = context.Memory(instruction); if (memory.kind == ResourceKind::ScalarBuffer) return ConstantU32(context.state, 0u); auto& state = context.state; - if (state.bufferMisalignment != 0u) return state.bufferMisalignment; - if (state.registrationModules) return Binary(state, spv::OpBitwiseAnd, TypeU32(state), context.Arg(*instruction.Argument(0)->Resolve(), 0u), ConstantU32(state, 3u)); return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords + 3u, 0u); } @@ -406,7 +404,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr auto function = state.formattedBufferFunctions.find(key); if (function == state.formattedBufferFunctions.end()) { const auto id = state.module.AllocateId(); - const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32); + const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32); const auto label = state.currentLabel; const auto byteOffset = state.memoryByteOffsets.at(element); struct Restore { @@ -417,24 +415,22 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr ~Restore() { state.module.EndHelperFunction(); state.memoryByteOffsets.at(element) = byteOffset; - state.bufferMisalignment = 0u; state.currentLabel = label; } }; state.module.BeginHelperFunction(); const Restore restore{state, element, label, byteOffset}; state.module.AddFunction(spv::OpFunction, resultType, id, spv::FunctionControlDontInlineMask, functionType); - std::array parameters{}; - for (std::uint32_t index = 0; index < (store ? 11u : 10u); ++index) { + std::array parameters{}; + for (std::uint32_t index = 0; index < (store ? 10u : 9u); ++index) { parameters[index] = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionParameter, index == 10u ? valueType : u32, parameters[index]); + state.module.AddFunction(spv::OpFunctionParameter, index == 9u ? valueType : u32, parameters[index]); } EmitLabel(state, state.module.AllocateId()); state.memoryByteOffsets.at(element) = parameters[4]; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; RuntimeDescriptor shared{}; if (state.registrationModules) { - state.bufferMisalignment = parameters[9]; const auto word3 = state.program.Info().buffers.at(memory.resource).descriptorFormatted ? state.module.SpecializationConstant(u32, first + 1u, 0u) : parameters[6]; shared = {ConstantU32(state, 0u), parameters[3], parameters[5], word3, nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; } else { @@ -444,7 +440,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], valid, parameters[1], parameters[2]}; } - const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[10] : 0u); + const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[9] : 0u); if (store) state.module.AddFunction(spv::OpReturn); else state.module.AddFunction(spv::OpReturnValue, value); state.module.AddFunction(spv::OpFunctionEnd); @@ -455,9 +451,8 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr const auto flag = [&](std::uint32_t condition) { return Select(state, u32, condition, ConstantU32(state, 1u), ConstantU32(state, 0u)); }; const auto swizzle = flag(resource.swizzle); const auto valid = flag(resource.valid); - const auto misalignment = baseMisalignment(context, instruction); - if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, misalignment, data); - else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, misalignment); + if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, data); + else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid); return store ? ConstantU32(state, 0u) : result; } From b1ee794a663d567f660971fa5e3ceb65b90960e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 09:59:23 +0200 Subject: [PATCH 298/318] test(agc): give the formatted buffer vertex case clip distances and the zero-position pixel shader its push slot Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp | 4 ++-- .../libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index 55fe8e55d..24ed571cd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -19,8 +19,8 @@ void Check(bool condition, const std::string& message) { } SpirvTarget Target() { - static constexpr std::array capabilities{ - spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, }; static constexpr std::array extensions{ "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp index b5c7ad5c5..db8010acb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp @@ -53,9 +53,9 @@ Counts RunCase(AgcDriver::VulkanDevice& device, const char* name, std::uint32_t pixelInfo.wave32 = true; pixelInfo.targetOutputMode[0] = 9u; pixelInfo.targetExportMapping.fill(0xe4u); - ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}}; + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; const auto pixel = ShaderRecompiler::Recompile(pixelRequest); - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}}; + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; From d37463e76f8cdbfa474b544b7c2ca679ae6a4afb Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 14:28:35 +1300 Subject: [PATCH 299/318] fix(shader): prove masked lanes unread for every select in one backward pass MaskedSelectEliminator walked forward from each select to see whether the lanes its mask excludes are read. Since fda4aa90 the walks pass explicit-LOD gathers, size queries and guarded image writes, so in a large pixel shader most of them cover the rest of the program, and the cost grows with the square of its size (#3504). The pass now computes, for every value, the set of masks under which it is unread, from its uses backwards: a use guarded by a condition contributes the masks that condition implies, a lane-local use adds what its result is unread under, and any other use contributes nothing. A select is dropped when its mask is in its own set. Loops are solved as strongly connected components. The rules for guards, lane-local uses, loops, whole quad masks, selected execs, rebuilt mask words and guarded first-lane reads are the ones the walk applied. A first-lane read passes the masks its read mask implies when its own result is unread outside that read mask, which is solved once per read in the same way. The walk's step limit, its 64-level implication depth and its first-lane nesting depth are gone, so a proof no longer depends on the order of the questions. --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 56 +- .../Optimization/MaskedSelectEliminator.hpp | 1 + .../src/MaskedSelectEliminator.cpp | 859 +++++++++++++----- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 672 insertions(+), 246 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 030146961..175fdaf97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -8,6 +8,7 @@ #include #include #include +#include using namespace ShaderRecompiler; @@ -539,25 +540,35 @@ int main() { return count == 0u && !gone; }); - const auto chained = [](bool wideRoot) { + const auto chained = [](bool wideRoot, std::uint32_t kills) { Builder b; auto& wide = b.mask(48u); auto& exec = b.logicalAnd(wide, b.mask(32u)); auto& old = b.lane(); auto& written = b.select(exec, b.add(old, 1u), old); IrValue* later = wideRoot ? &wide : &exec; - for (std::uint32_t kill = 0; kill < 24u; ++kill) later = &b.logicalAnd(*later, b.mask(31u - kill)); + for (std::uint32_t kill = 0; kill < kills; ++kill) later = &b.logicalAnd(*later, b.mask(31u - kill % 24u)); b.keep(b.select(*later, b.add(written, 2u), old)); return std::pair{b.eliminate(), removed(written)}; }; passed &= run("an exec narrowed by a long chain of kills still implies the write's exec", [&] { - const auto [count, gone] = chained(false); + const auto [count, gone] = chained(false, 24u); return count == 1u && gone; }); passed &= run("a long chain of kills rooted at a wider exec keeps the select", [&] { - const auto [count, gone] = chained(true); + const auto [count, gone] = chained(true, 24u); + return count == 0u && !gone; + }); + + passed &= run("an exec narrowed by hundreds of kills still implies the write's exec", [&] { + const auto [count, gone] = chained(false, 300u); + return count == 1u && gone; + }); + + passed &= run("hundreds of kills rooted at a wider exec keep the select", [&] { + const auto [count, gone] = chained(true, 300u); return count == 0u && !gone; }); @@ -891,5 +902,42 @@ int main() { return b.eliminate() == 0u && !removed(written); }); + const auto shaded = [](std::uint32_t units) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.imageSampleFlags = RdnaImageSampleFlagLevelZero; + b.program.Resources().memoryInfo.push_back(memory); + MemoryFlags flags{0u, 0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& exec = b.mask(48u); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + IrValue* color = &b.lane(); + IrValue* address = nullptr; + std::uint32_t kept = 0; + std::vector compared; + for (std::uint32_t unit = 0; unit < units; ++unit) { + auto& sum = b.add(*color, unit); + auto& low = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&sum, &b.constant(100u + unit)}); + compared.push_back(&b.select(low, sum, b.add(*color, 1u))); + address = &b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {compared.back(), color}); + auto& gathered = b.emit(IrOpcode::ImageGatherRaw, IrType::U32x4, {&image, &sampler, address}, bits); + color = &b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&gathered, &b.constant(0u)}), *color); + } + b.emit(IrOpcode::ImageWrite, IrType::Void, {&image, address, &b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {color, color, color, color}), &exec}); + const auto stats = MaskedSelectEliminator{}.Eliminate(b.program); + for (const IrValue* select : compared) kept += removed(*select) ? 0u : 1u; + return std::pair{stats, kept}; + }; + + passed &= run("selects that each reach the rest of the program are settled in steps that grow with the program, not with its square", [&] { + const auto [small, smallKept] = shaded(500u); + const auto [large, largeKept] = shaded(4000u); + return small.removedSelects == 500u && smallKept == 500u && large.removedSelects == 4000u && largeKept == 4000u && small.steps != 0u && large.steps <= 9u * small.steps; + }); + return passed ? 0 : 1; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp index fad7eed9c..2e0b5bd5b 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp @@ -8,6 +8,7 @@ namespace ShaderRecompiler { struct MaskedSelectEliminationStats { std::uint32_t removedSelects = 0; + std::uint64_t steps = 0; }; class MaskedSelectEliminator { diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 0ab716a84..61a65c21b 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -2,6 +2,12 @@ #include "Optimization/DeadCodeEliminator.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -11,8 +17,6 @@ namespace ShaderRecompiler { namespace { -constexpr std::size_t MinimumWalkLimit = 4096; -constexpr std::size_t WalkLimitPerInstruction = 2; constexpr std::uint32_t DepthLimit = 8; bool isSelect(IrOpcode opcode) { @@ -239,6 +243,7 @@ const IrValue* otherArgument(const IrValue* value, std::uint32_t immediate) { struct ThreadBit { const IrValue* low = nullptr; const IrValue* high = nullptr; + const IrValue* word = nullptr; }; bool isLane(const IrValue* value) { @@ -257,11 +262,11 @@ std::optional threadBit(const IrValue* value, std::uint32_t waveSize) const IrValue* lane = otherArgument(amount, 31u); if (lane == nullptr || !isLane(lane)) return std::nullopt; const IrValue* word = shifted->Argument(0)->Resolve(); - if (waveSize == 32u || word->HasImmediate()) return ThreadBit{word, word}; + if (waveSize == 32u || word->HasImmediate()) return ThreadBit{word, word, word}; if (word->Opcode() != IrOpcode::SelectU32) return std::nullopt; const IrValue* half = word->Argument(0)->Resolve(); if (half->Opcode() != IrOpcode::ULessThan32 || !isLane(half->Argument(0)) || !isImmediate(half->Argument(1)->Resolve(), 32u)) return std::nullopt; - return ThreadBit{word->Argument(1)->Resolve(), word->Argument(2)->Resolve()}; + return ThreadBit{word->Argument(1)->Resolve(), word->Argument(2)->Resolve(), word}; } bool alwaysTrue(const IrValue* value, std::uint32_t waveSize, std::uint32_t depth = 0) { @@ -326,11 +331,6 @@ const IrValue* wholeQuadSource(const IrValue* mask, std::uint32_t waveSize) { return source->Opcode() == IrOpcode::CompositeConstructU64 ? source : nullptr; } -bool withinQuadsOf(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, const BlockSet& uniform) { - const IrValue* source = wholeQuadSource(mask, waveSize); - return source != nullptr && withinWords(condition, source->Argument(0)->Resolve(), source->Argument(1)->Resolve(), waveSize, uniform, 0u); -} - enum class Region : std::uint8_t { Unreached, Once, Loop }; Region regionOf(const std::vector& regions, const IrBlock* block) { @@ -342,158 +342,6 @@ bool followable(const std::vector& regions, const IrBlock* block, bool a return region == Region::Once || (acrossLoops && region == Region::Loop); } -struct ImplicationKey { - const IrValue* first = nullptr; - const IrValue* second = nullptr; - - bool operator==(const ImplicationKey&) const = default; -}; - -struct ImplicationKeyHash { - std::size_t operator()(const ImplicationKey& key) const { - return std::hash{}(key.first) * 31u ^ std::hash{}(key.second); - } -}; - -class Implications { -public: - Implications(const std::vector& regions, const BlockSet& uniform, std::uint32_t waveSize, std::size_t& work) : regions(regions), uniform(uniform), waveSize(waveSize), work(work) {} - - void Reset(const IrValue* writeMask, bool acrossLoops) { - mask = writeMask; - loops = acrossLoops; - always = alwaysTrue(writeMask, waveSize); - results.clear(); - proven.clear(); - assumptionReads = 0; - } - - bool Implies(const IrValue* condition, std::uint32_t depth = 0) { - condition = condition->Resolve(); - if (condition == mask || always) return true; - if (condition->HasImmediate()) return condition->Type() == IrType::Bool && !condition->ImmediateBool(); - const ImplicationKey key{condition, nullptr}; - if (const auto known = lookup(key)) return *known; - if (depth > ImplicationDepthLimit || work == 0) return false; - --work; - const Attempt attempt = assume(key); - bool result = false; - if (withinQuadsOf(condition, mask, waveSize, uniform)) { - result = true; - } else if (condition->Opcode() == IrOpcode::LogicalAnd) { - result = Implies(condition->Argument(0), depth + 1u) || Implies(condition->Argument(1), depth + 1u); - } else if (condition->Opcode() == IrOpcode::SelectU1) { - const IrValue* selector = condition->Argument(0)->Resolve(); - const bool trueCase = Implies(condition->Argument(1), depth + 1u) || Implies(selector, depth + 1u); - const bool falseCase = Implies(condition->Argument(2), depth + 1u) || (selector->Opcode() == IrOpcode::LogicalNot && Implies(selector->Argument(0), depth + 1u)); - result = trueCase && falseCase; - } else if (condition->IsPhi()) { - result = merges(*condition); - for (std::size_t index = 0; result && index < condition->ArgumentCount(); ++index) { - result = Implies(condition->Argument(index), depth + 1u); - } - } else if (const auto bit = threadBit(condition, waveSize)) { - result = wordsImply(bit->low, bit->high, depth + 1u); - } - return settle(key, attempt, result); - } - -private: - static constexpr std::uint32_t ImplicationDepthLimit = 64; - - enum class Proof : std::uint8_t { Assumed, Implied, NotImplied }; - - struct Attempt { - std::size_t proven = 0; - std::size_t assumptionReads = 0; - }; - - std::optional lookup(const ImplicationKey& key) { - const auto known = results.find(key); - if (known == results.end()) return std::nullopt; - if (known->second == Proof::Assumed) ++assumptionReads; - return known->second != Proof::NotImplied; - } - - Attempt assume(const ImplicationKey& key) { - results.emplace(key, Proof::Assumed); - return Attempt{proven.size(), assumptionReads}; - } - - bool settle(const ImplicationKey& key, const Attempt& attempt, bool implied) { - if (!implied && assumptionReads != attempt.assumptionReads) { - for (std::size_t index = attempt.proven; index < proven.size(); ++index) results.erase(proven[index]); - proven.resize(attempt.proven); - } - results[key] = implied ? Proof::Implied : Proof::NotImplied; - if (implied) proven.push_back(key); - return implied; - } - - bool merges(const IrValue& phi) const { - if (!followable(regions, phi.Parent(), loops) || phi.ArgumentCount() != phi.PhiBlockCount()) return false; - for (std::size_t index = 0; index < phi.ArgumentCount(); ++index) { - const IrValue* incoming = phi.Argument(index)->Resolve(); - if (!incoming->HasImmediate() && !followable(regions, incoming->Parent(), loops)) return false; - } - return true; - } - - static const IrValue* incomingFrom(const IrValue& phi, const IrBlock* predecessor) { - for (std::size_t index = 0; index < phi.PhiBlockCount(); ++index) { - if (phi.PhiBlock(index) == predecessor) return phi.Argument(index)->Resolve(); - } - return nullptr; - } - - bool wordsImply(const IrValue* low, const IrValue* high, std::uint32_t depth) { - if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; - const auto maskBit = threadBit(mask, waveSize); - if (maskBit && low == maskBit->low && (waveSize == 32u || high == maskBit->high)) return true; - const ImplicationKey key{low, high}; - if (const auto known = lookup(key)) return *known; - if (depth > ImplicationDepthLimit || work == 0) return false; - --work; - const Attempt attempt = assume(key); - bool result = false; - if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { - const IrValue* ballot = low->Argument(0)->Resolve(); - const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); - result = ballot->Opcode() == IrOpcode::Ballot && sameBallot && Implies(ballot->Argument(0), depth + 1u); - } - if (!result && low->Opcode() == IrOpcode::BitwiseAnd32) { - if (waveSize == 32u) { - result = wordsImply(low->Argument(0)->Resolve(), high, depth + 1u) || wordsImply(low->Argument(1)->Resolve(), high, depth + 1u); - } else if (high->Opcode() == IrOpcode::BitwiseAnd32) { - for (std::size_t lowIndex = 0; !result && lowIndex < 2u; ++lowIndex) { - for (std::size_t highIndex = 0; !result && highIndex < 2u; ++highIndex) { - result = wordsImply(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve(), depth + 1u); - } - } - } - } - if (!result && low->IsPhi() && merges(*low) && (waveSize == 32u || (high->IsPhi() && high->Parent() == low->Parent() && merges(*high)))) { - result = true; - for (std::size_t index = 0; result && index < low->PhiBlockCount(); ++index) { - const IrValue* highIncoming = waveSize == 32u ? high : incomingFrom(*high, low->PhiBlock(index)); - result = highIncoming != nullptr && wordsImply(low->Argument(index)->Resolve(), highIncoming, depth + 1u); - } - } - return settle(key, attempt, result); - } - - const std::vector& regions; - const BlockSet& uniform; - std::uint32_t waveSize; - std::size_t& work; - const IrValue* mask = nullptr; - bool loops = false; - bool always = false; - std::unordered_map results; - std::vector proven; - std::size_t assumptionReads = 0; -}; - std::vector blockRegions(const IrProgram& program) { const auto& order = program.BlockOrder(); if (order.empty()) return {}; @@ -536,108 +384,637 @@ std::vector blockRegions(const IrProgram& program) { return result; } -std::size_t walkLimit(const IrProgram& program) { - std::size_t instructions = 0; - for (const IrBlock* block : program.BlockOrder()) instructions += block->Instructions().size(); - return std::max(MinimumWalkLimit, WalkLimitPerInstruction * instructions); -} +using MaskSet = std::uint32_t; -class MaskedLaneWalk { +class MaskSets { public: - MaskedLaneWalk(const IrProgram& program, const std::vector& regions, std::uint32_t nesting = 0) : program(program), regions(regions), uniform(uniformEntryBlocks(program)), limit(walkLimit(program)), implications(regions, uniform, program.WaveSize(), work), nesting(nesting) {} + static constexpr MaskSet None = 0; + static constexpr MaskSet All = 1; - bool UnobservedWhereMasked(const IrValue& select, const IrValue* mask) { - const bool acrossLoops = mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once; - const bool sameIteration = !acrossLoops && select.Opcode() == IrOpcode::ReadFirstLane; - work = limit; - implications.Reset(mask, acrossLoops); - const bool quadMasked = program.Resources().stage == IrShaderStage::Pixel && wholeQuadSource(mask, program.WaveSize()) != nullptr; - startVisits(); - pending.clear(); - pending.push_back(&select); - static_cast(firstVisit(select)); - while (!pending.empty()) { - const IrValue* value = pending.back(); - pending.pop_back(); - for (const IrUse& use : value->OperandUses()) { - const IrValue* user = use.user; - if (sameIteration ? user->Parent() != select.Parent() || user->IsPhi() : !followable(regions, user->Parent(), acrossLoops)) return false; - if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; - if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; - if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; - if (user->Opcode() == IrOpcode::ReadFirstLane && use.operand == 0u && nesting < DepthLimit && implications.Implies(user->Argument(1))) { - MaskedLaneWalk nested(program, regions, nesting + 1u); - nested.limit = work; - const bool unobserved = nested.UnobservedWhereMasked(*user, user->Argument(1)->Resolve()); - work = nested.work; - if (unobserved) continue; - } - if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user) && !(quadMasked && user->Opcode() == IrOpcode::ImageSampleRaw)) return false; - if (firstVisit(*user)) { - if (work == 0) return false; - --work; - pending.push_back(user); + explicit MaskSets(std::uint64_t& steps) : steps(steps), sets(2) {} + + MaskSet Single(std::uint32_t mask) { + sets.push_back({mask}); + return static_cast(sets.size() - 1u); + } + + MaskSet Of(std::vector&& masks) { + if (masks.empty()) return None; + std::sort(masks.begin(), masks.end()); + sets.push_back(std::move(masks)); + return static_cast(sets.size() - 1u); + } + + MaskSet Union(MaskSet left, MaskSet right) { + if (left == right || right == None) return left; + if (left == None) return right; + if (left == All || right == All) return All; + std::vector merged; + merged.reserve(sets[left].size() + sets[right].size()); + std::set_union(sets[left].begin(), sets[left].end(), sets[right].begin(), sets[right].end(), std::back_inserter(merged)); + return keep(left, right, std::move(merged)); + } + + MaskSet Intersection(MaskSet left, MaskSet right) { + if (left == right || right == All) return left; + if (left == All) return right; + if (left == None || right == None) return None; + std::vector common; + std::set_intersection(sets[left].begin(), sets[left].end(), sets[right].begin(), sets[right].end(), std::back_inserter(common)); + return keep(left, right, std::move(common)); + } + + [[nodiscard]] bool Contains(MaskSet set, std::uint32_t mask) const { + return set == All || (set != None && std::binary_search(sets[set].begin(), sets[set].end(), mask)); + } + + [[nodiscard]] bool Same(MaskSet left, MaskSet right) const { + return left == right || (left > All && right > All && sets[left] == sets[right]); + } + +private: + MaskSet keep(MaskSet left, MaskSet right, std::vector&& result) { + steps += sets[left].size() + sets[right].size(); + if (result.empty()) return None; + if (result.size() == sets[left].size()) return left; + if (result.size() == sets[right].size()) return right; + sets.push_back(std::move(result)); + return static_cast(sets.size() - 1u); + } + + std::uint64_t& steps; + std::vector> sets; +}; + +class Components { +public: + template + void From(std::uint32_t root, TSuccessors&& successors, TVisit&& visit) { + if (seen(root)) return; + open(root, successors); + while (!frames.empty()) { + Frame& frame = frames.back(); + if (frame.next != frame.end) { + const std::uint32_t successor = edges[frame.next++]; + if (successor == frame.node) frame.cyclic = true; + if (!seen(successor)) { + open(successor, successors); + } else if (low[successor] != Closed) { + low[frame.node] = std::min(low[frame.node], order[successor]); } + continue; } + const std::uint32_t node = frame.node; + const bool cyclic = frame.cyclic; + edges.resize(frame.begin); + frames.pop_back(); + if (!frames.empty()) low[frames.back().node] = std::min(low[frames.back().node], low[node]); + if (low[node] != order[node]) continue; + members.clear(); + do { + members.push_back(pending.back()); + pending.pop_back(); + low[members.back()] = Closed; + } while (members.back() != node); + visit(members, cyclic || members.size() != 1u); + } + } + +private: + static constexpr std::uint32_t Closed = 0xffffffffu; + + struct Frame { + std::uint32_t node = 0; + std::size_t begin = 0; + std::size_t next = 0; + std::size_t end = 0; + bool cyclic = false; + }; + + [[nodiscard]] bool seen(std::uint32_t node) const { + return node < order.size() && order[node] != 0u; + } + + template + void open(std::uint32_t node, TSuccessors& successors) { + if (node >= order.size()) { + order.resize(node + 1u, 0u); + low.resize(node + 1u, 0u); + } + order[node] = low[node] = ++opened; + pending.push_back(node); + const std::size_t begin = edges.size(); + successors(node, edges); + frames.push_back(Frame{node, begin, begin, edges.size(), false}); + } + + std::vector order; + std::vector low; + std::vector pending; + std::vector edges; + std::vector members; + std::vector frames; + std::uint32_t opened = 0; +}; + +struct MaskFamily { + bool acrossLoops = false; + bool wholeQuads = false; + + bool operator==(const MaskFamily&) const = default; +}; + +enum class Implication : std::uint8_t { Condition, Words, SelectedTrue, SelectedFalse }; + +struct ImplicationKey { + const IrValue* first = nullptr; + const IrValue* second = nullptr; + Implication kind = Implication::Condition; + + bool operator==(const ImplicationKey&) const = default; +}; + +struct ImplicationKeyHash { + std::size_t operator()(const ImplicationKey& key) const { + return std::hash{}(key.first) * 31u ^ std::hash{}(key.second); + } +}; + +class ImpliedMasks { +public: + ImpliedMasks(const std::vector& regions, const BlockSet& uniform, std::uint32_t waveSize, MaskFamily family, MaskSets& sets, std::uint64_t& steps) : regions(regions), uniform(uniform), waveSize(waveSize), family(family), sets(sets), steps(steps) {} + + void AddMask(const IrValue* mask) { + if (!mark(masks, mask)) return; + const auto bit = threadBit(mask, waveSize); + if (bit) { + mark(followed, bit->word); + sameWords[wordsKey(bit->low, bit->high)].masks.push_back(mask->Id()); + } + if (!family.wholeQuads) return; + mark(followed, mask); + const IrValue* source = wholeQuadSource(mask, waveSize); + const auto known = std::find_if(quads.begin(), quads.end(), [&](const Quads& entry) { return entry.source == source; }); + if (known == quads.end()) { + quads.push_back(Quads{source, sets.Single(mask->Id())}); + } else { + known->masks = sets.Union(known->masks, sets.Single(mask->Id())); + } + } + + MaskSet Of(const IrValue* guard) { + const std::uint32_t root = condition(guard); + components.From(root, [this](std::uint32_t node, std::vector& out) { expand(node, out); }, [this](const std::vector& members, bool cyclic) { settle(members, cyclic); }); + return nodes[root].implied; + } + + [[nodiscard]] bool Follows(const IrValue& value) const { + return value.Id() < followed.size() && followed[value.Id()] != 0u; + } + +private: + struct Node { + ImplicationKey key; + MaskSet own = MaskSets::None; + MaskSet implied = MaskSets::All; + bool every = false; + bool queued = false; + std::uint32_t group = 0; + std::vector inputs; + std::vector outputs; + }; + + struct Quads { + const IrValue* source = nullptr; + MaskSet masks = MaskSets::None; + }; + + struct SameWords { + std::vector masks; + MaskSet set = MaskSets::None; + }; + + static bool mark(std::vector& marks, const IrValue* value) { + if (value->Id() >= marks.size()) marks.resize(value->Id() + 1u, 0u); + return std::exchange(marks[value->Id()], std::uint8_t{1}) == 0u; + } + + std::uint32_t add(const ImplicationKey& key) { + nodes.push_back(Node{key}); + mark(followed, key.first); + if (key.second != nullptr) mark(followed, key.second); + return static_cast(nodes.size() - 1u); + } + + [[nodiscard]] ImplicationKey wordsKey(const IrValue* low, const IrValue* high) const { + return ImplicationKey{low, waveSize == 32u ? low : high, Implication::Words}; + } + + std::uint32_t words(const IrValue* low, const IrValue* high) { + const ImplicationKey key = wordsKey(low, high); + const auto known = wordNodes.find(key); + return known != wordNodes.end() ? known->second : wordNodes.emplace(key, add(key)).first->second; + } + + std::uint32_t condition(const IrValue* value) { + value = value->Resolve(); + if (value->Id() >= conditionNodes.size()) conditionNodes.resize(value->Id() + 1u, Unknown); + if (conditionNodes[value->Id()] == Unknown) conditionNodes[value->Id()] = add(ImplicationKey{value}); + return conditionNodes[value->Id()]; + } + + bool merges(const IrValue& phi) const { + if (!followable(regions, phi.Parent(), family.acrossLoops) || phi.ArgumentCount() != phi.PhiBlockCount()) return false; + for (std::size_t index = 0; index < phi.ArgumentCount(); ++index) { + const IrValue* incoming = phi.Argument(index)->Resolve(); + if (!incoming->HasImmediate() && !followable(regions, incoming->Parent(), family.acrossLoops)) return false; } return true; } -private: - void startVisits() { - if (++epoch != 0u) return; - std::fill(visits.begin(), visits.end(), 0u); - epoch = 1u; + static const IrValue* incomingFrom(const IrValue& phi, const IrBlock* predecessor) { + for (std::size_t index = 0; index < phi.PhiBlockCount(); ++index) { + if (phi.PhiBlock(index) == predecessor) return phi.Argument(index)->Resolve(); + } + return nullptr; } - bool firstVisit(const IrValue& value) { - const std::uint32_t id = value.Id(); - if (id >= visits.size()) visits.resize(id + 1u, 0u); - if (visits[id] == epoch) return false; - visits[id] = epoch; - return true; + void expand(std::uint32_t node, std::vector& out) { + const ImplicationKey key = nodes[node].key; + Node shape{key}; + if (key.kind == Implication::Condition) { + expandCondition(key.first, shape); + } else if (key.kind == Implication::Words) { + expandWords(key.first, key.second, shape); + } else { + expandSelected(key.first, key.kind == Implication::SelectedTrue, shape); + } + out.insert(out.end(), shape.inputs.begin(), shape.inputs.end()); + for (const std::uint32_t input : shape.inputs) nodes[input].outputs.push_back(node); + shape.outputs = std::move(nodes[node].outputs); + nodes[node] = std::move(shape); } - const IrProgram& program; + void expandCondition(const IrValue* value, Node& shape) { + if (value->Id() < masks.size() && masks[value->Id()] != 0u && !threadBit(value, waveSize)) shape.own = sets.Single(value->Id()); + if (value->HasImmediate()) { + if (value->Type() == IrType::Bool && !value->ImmediateBool()) shape.own = MaskSets::All; + return; + } + for (const Quads& entry : quads) { + ++steps; + if (withinWords(value, entry.source->Argument(0)->Resolve(), entry.source->Argument(1)->Resolve(), waveSize, uniform, 0u)) shape.own = sets.Union(shape.own, entry.masks); + } + if (value->Opcode() == IrOpcode::LogicalAnd) { + shape.inputs = {condition(value->Argument(0)), condition(value->Argument(1))}; + } else if (value->Opcode() == IrOpcode::SelectU1) { + shape.every = true; + shape.inputs = {add(ImplicationKey{value, nullptr, Implication::SelectedTrue}), add(ImplicationKey{value, nullptr, Implication::SelectedFalse})}; + } else if (value->IsPhi()) { + if (!merges(*value)) return; + shape.every = true; + for (std::size_t index = 0; index < value->ArgumentCount(); ++index) shape.inputs.push_back(condition(value->Argument(index))); + } else if (const auto bit = threadBit(value, waveSize)) { + mark(followed, bit->word); + shape.inputs = {words(bit->low, bit->high)}; + } + } + + void expandSelected(const IrValue* select, bool whenTrue, Node& shape) { + const IrValue* selector = select->Argument(0)->Resolve(); + shape.inputs = {condition(select->Argument(whenTrue ? 1u : 2u))}; + if (whenTrue) { + shape.inputs.push_back(condition(selector)); + } else if (selector->Opcode() == IrOpcode::LogicalNot) { + shape.inputs.push_back(condition(selector->Argument(0))); + } + } + + void expandWords(const IrValue* low, const IrValue* high, Node& shape) { + if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) { + shape.own = MaskSets::All; + return; + } + if (const auto same = sameWords.find(wordsKey(low, high)); same != sameWords.end()) { + if (same->second.set == MaskSets::None) same->second.set = sets.Of(std::move(same->second.masks)); + shape.own = same->second.set; + } + if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { + const IrValue* ballot = low->Argument(0)->Resolve(); + const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); + if (ballot->Opcode() == IrOpcode::Ballot && sameBallot) shape.inputs = {condition(ballot->Argument(0))}; + } else if (low->Opcode() == IrOpcode::BitwiseAnd32) { + if (waveSize == 32u) { + shape.inputs = {words(low->Argument(0)->Resolve(), high), words(low->Argument(1)->Resolve(), high)}; + } else if (high->Opcode() == IrOpcode::BitwiseAnd32) { + for (std::size_t lowIndex = 0; lowIndex < 2u; ++lowIndex) { + for (std::size_t highIndex = 0; highIndex < 2u; ++highIndex) shape.inputs.push_back(words(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve())); + } + } + } else if (low->IsPhi() && merges(*low) && (waveSize == 32u || (high->IsPhi() && high->Parent() == low->Parent() && merges(*high)))) { + for (std::size_t index = 0; index < low->PhiBlockCount(); ++index) { + const IrValue* highIncoming = waveSize == 32u ? high : incomingFrom(*high, low->PhiBlock(index)); + if (highIncoming == nullptr) { + shape.inputs.clear(); + return; + } + shape.inputs.push_back(words(low->Argument(index)->Resolve(), highIncoming)); + } + shape.every = true; + } + } + + MaskSet evaluate(const Node& node) { + MaskSet result = node.every ? MaskSets::All : MaskSets::None; + for (const std::uint32_t input : node.inputs) { + ++steps; + result = node.every ? sets.Intersection(result, nodes[input].implied) : sets.Union(result, nodes[input].implied); + } + return sets.Union(result, node.own); + } + + void settle(const std::vector& members, bool cyclic) { + if (!cyclic) { + nodes[members.front()].implied = evaluate(nodes[members.front()]); + return; + } + ++groups; + queue.assign(members.rbegin(), members.rend()); + for (const std::uint32_t member : members) { + nodes[member].queued = true; + nodes[member].group = groups; + } + while (!queue.empty()) { + const std::uint32_t member = queue.back(); + queue.pop_back(); + nodes[member].queued = false; + const MaskSet result = evaluate(nodes[member]); + if (sets.Same(result, nodes[member].implied)) continue; + nodes[member].implied = result; + for (const std::uint32_t output : nodes[member].outputs) { + if (nodes[output].group == groups && !std::exchange(nodes[output].queued, true)) queue.push_back(output); + } + } + } + + static constexpr std::uint32_t Unknown = 0xffffffffu; + const std::vector& regions; - BlockSet uniform; - std::size_t limit; - std::size_t work = 0; - Implications implications; - std::vector pending; - std::vector visits; - std::uint32_t epoch = 0; - std::uint32_t nesting = 0; + const BlockSet& uniform; + std::uint32_t waveSize; + MaskFamily family; + MaskSets& sets; + std::uint64_t& steps; + std::vector masks; + std::vector followed; + std::vector quads; + std::vector nodes; + std::vector conditionNodes; + std::unordered_map wordNodes; + std::unordered_map sameWords; + std::vector queue; + std::uint32_t groups = 0; + Components components; }; +MaskFamily familyOf(const std::vector& regions, const IrValue* mask, std::uint32_t waveSize) { + return MaskFamily{mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once, wholeQuadSource(mask, waveSize) != nullptr}; +} + +class UnreadLanes; + +using FirstLaneReads = std::array, 4>; + +class UnreadLanes { +public: + UnreadLanes(IrProgram& program, const std::vector& regions, const BlockSet& uniform, MaskFamily family, std::uint64_t& steps, FirstLaneReads* shared = nullptr) + : program(program), regions(regions), uniform(uniform), family(family), sampledByQuad(family.wholeQuads && program.Resources().stage == IrShaderStage::Pixel), firstLane(shared != nullptr), sameBlock(firstLane && !family.acrossLoops), steps(steps), sets(steps), implied(regions, uniform, program.WaveSize(), family, sets, steps), firstLanes(firstLane ? *shared : owned) { + if (!firstLane) return; + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::ReadFirstLane) continue; + const IrValue* mask = inst->Argument(1)->Resolve(); + if (familyOf(regions, mask, program.WaveSize()) == family) implied.AddMask(mask); + } + } + } + + bool UnreadOutsideItsMask(IrValue& read) { + if (solving) return false; + solving = true; + static_cast(readerOf(read)); + components.From(read.Id(), [this](std::uint32_t node, std::vector& out) { expand(node, out); }, [this](const std::vector& members, bool cyclic) { settle(members, cyclic); }); + solving = false; + return sets.Contains(unread[read.Id()], read.Argument(1)->Resolve()->Id()); + } + + [[nodiscard]] bool Follows(const IrValue& value) const { + return implied.Follows(value) || (value.Id() < readers.size() && readers[value.Id()] != Reader::Unknown); + } + + std::uint32_t Remove() { + std::vector order; + for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { + const bool single = regionOf(regions, *blockIt) == Region::Once; + const auto& instructions = (*blockIt)->Instructions(); + for (auto it = instructions.rbegin(); it != instructions.rend(); ++it) { + IrValue* inst = *it; + order.push_back(inst); + if (!single || !isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses()) continue; + const IrValue* mask = inst->Argument(0)->Resolve(); + if (familyOf(regions, mask, program.WaveSize()) != family) continue; + implied.AddMask(mask); + slot(writes, inst->Id(), static_cast(nullptr)) = mask; + found = true; + } + } + if (!found) return 0; + for (IrValue* inst : order) { + if (halted) break; + static_cast(readerOf(*inst)); + components.From(inst->Id(), [this](std::uint32_t node, std::vector& out) { expand(node, out); }, [this](const std::vector& members, bool cyclic) { settle(members, cyclic); }); + } + if (removed != 0u) { + for (IrBlock* block : program.BlockOrder()) std::erase_if(block->Instructions(), [](const IrValue* inst) { return inst->Parent() == nullptr; }); + } + return removed; + } + +private: + static constexpr std::uint32_t Queued = 0xffffffffu; + + template + static TValue& slot(std::vector& table, std::uint32_t id, TValue fill) { + if (id >= table.size()) table.resize(id + 1u, fill); + return table[id]; + } + + enum class Reader : std::uint8_t { Unknown, Outside, Reads, Passes }; + + Reader readerOf(IrValue& user) { + Reader& known = slot(readers, user.Id(), Reader::Unknown); + if (known != Reader::Unknown) return known; + slot(values, user.Id(), static_cast(nullptr)) = &user; + slot(unread, user.Id(), MaskSets::All); + slot(groups, user.Id(), std::uint32_t{0}); + if (sameBlock ? user.IsPhi() : !followable(regions, user.Parent(), family.acrossLoops)) return known = Reader::Outside; + const bool passes = user.IsPhi() || isLaneLocal(user.Opcode()) || isExplicitLodSample(program, user) || (sampledByQuad && user.Opcode() == IrOpcode::ImageSampleRaw); + return known = passes ? Reader::Passes : Reader::Reads; + } + + [[nodiscard]] bool leaves(const IrValue& value, const IrValue& user) const { + return sameBlock && user.Parent() != value.Parent(); + } + + [[nodiscard]] bool readsFirstLane(const IrUse& use) const { + return use.user->Opcode() == IrOpcode::ReadFirstLane && use.operand == 0u; + } + + [[nodiscard]] bool solvedHere(const IrValue& read) const { + return firstLane && familyOf(regions, read.Argument(1)->Resolve(), program.WaveSize()) == family; + } + + bool unreadOutsideItsMask(IrValue& read) { + const IrValue* mask = read.Argument(1)->Resolve(); + if (solvedHere(read)) return sets.Contains(unread[read.Id()], mask->Id()); + const MaskFamily readFamily = familyOf(regions, mask, program.WaveSize()); + auto& reads = firstLanes[(readFamily.acrossLoops ? 2u : 0u) + (readFamily.wholeQuads ? 1u : 0u)]; + if (!reads) reads = std::make_unique(program, regions, uniform, readFamily, steps, &firstLanes); + return reads->UnreadOutsideItsMask(read); + } + + void expand(std::uint32_t node, std::vector& out) { + for (const IrUse& use : values[node]->OperandUses()) { + const Reader reader = readerOf(*use.user); + if (reader == Reader::Outside || leaves(*values[node], *use.user)) continue; + if (reader == Reader::Passes || (readsFirstLane(use) && solvedHere(*use.user))) out.push_back(use.user->Id()); + } + } + + MaskSet unreadBy(const IrValue& value, const IrUse& use) { + IrValue* user = use.user; + const Reader reader = readerOf(*user); + if (reader == Reader::Outside || leaves(value, *user)) return MaskSets::None; + MaskSet guarded = MaskSets::None; + if (isSelect(user->Opcode()) && use.operand == 1u) { + guarded = implied.Of(user->Argument(0)); + } else if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u) { + guarded = implied.Of(user->Argument(1u - use.operand)); + } else if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount()) { + guarded = implied.Of(user->Argument(user->ArgumentCount() - 1u)); + } else if (readsFirstLane(use)) { + guarded = implied.Of(user->Argument(1)); + if (guarded != MaskSets::None && !unreadOutsideItsMask(*user)) guarded = MaskSets::None; + } + if (guarded == MaskSets::All || reader != Reader::Passes) return guarded; + return sets.Union(guarded, unread[user->Id()]); + } + + MaskSet evaluate(std::uint32_t node) { + MaskSet result = MaskSets::All; + for (const IrUse& use : values[node]->OperandUses()) { + if (result == MaskSets::None) break; + ++steps; + result = sets.Intersection(result, unreadBy(*values[node], use)); + } + return result; + } + + void settle(const std::vector& members, bool cyclic) { + if (halted) return; + if (!cyclic) { + unread[members.front()] = evaluate(members.front()); + remove(members.front()); + return; + } + ++group; + queue.assign(members.rbegin(), members.rend()); + for (const std::uint32_t member : members) groups[member] = Queued; + while (!queue.empty()) { + const std::uint32_t member = queue.back(); + queue.pop_back(); + groups[member] = group; + const MaskSet result = evaluate(member); + if (sets.Same(result, unread[member])) continue; + unread[member] = result; + for (const IrValue* argument : values[member]->Arguments()) { + if (argument == nullptr || argument->Id() >= groups.size() || groups[argument->Id()] != group) continue; + groups[argument->Id()] = Queued; + queue.push_back(argument->Id()); + } + } + } + + void remove(std::uint32_t node) { + IrValue* select = values[node]; + const IrValue* mask = node < writes.size() ? writes[node] : nullptr; + if (mask == nullptr || !select->HasUses() || select->Argument(0)->Resolve() != mask || !sets.Contains(unread[node], mask->Id())) return; + halted = implied.Follows(*select) || std::any_of(owned.begin(), owned.end(), [&](const auto& reads) { return reads && reads->Follows(*select); }); + select->ReplaceAllUsesWith(select->Argument(1)); + select->Invalidate(); + select->SetParent(nullptr); + ++removed; + } + + IrProgram& program; + const std::vector& regions; + const BlockSet& uniform; + MaskFamily family; + bool sampledByQuad; + bool firstLane; + bool sameBlock; + std::uint64_t& steps; + MaskSets sets; + ImpliedMasks implied; + FirstLaneReads owned; + FirstLaneReads& firstLanes; + Components components; + std::vector values; + std::vector writes; + std::vector unread; + std::vector readers; + std::vector groups; + std::vector queue; + std::uint32_t group = 0; + std::uint32_t removed = 0; + bool found = false; + bool halted = false; + bool solving = false; +}; + +std::uint32_t removeFullWaveSelects(IrProgram& program) { + std::uint32_t removed = 0; + for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { + auto& instructions = (*blockIt)->Instructions(); + auto it = instructions.end(); + while (it != instructions.begin()) { + --it; + IrValue* inst = *it; + if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses() || !alwaysTrue(inst->Argument(0), program.WaveSize())) continue; + inst->ReplaceAllUsesWith(inst->Argument(1)); + inst->Invalidate(); + inst->SetParent(nullptr); + it = instructions.erase(it); + ++removed; + } + } + return removed; +} + } MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& program) const { MaskedSelectEliminationStats stats; const auto regions = blockRegions(program); - MaskedLaneWalk walk(program, regions); - bool changed = true; - while (changed) { - changed = false; - for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { - const bool single = regionOf(regions, *blockIt) == Region::Once; - auto& instructions = (*blockIt)->Instructions(); - auto it = instructions.end(); - while (it != instructions.begin()) { - --it; - IrValue* inst = *it; - if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses()) continue; - const IrValue* mask = inst->Argument(0)->Resolve(); - if (!alwaysTrue(mask, program.WaveSize()) && (!single || !walk.UnobservedWhereMasked(*inst, mask))) continue; - inst->ReplaceAllUsesWith(inst->Argument(1)); - inst->Invalidate(); - inst->SetParent(nullptr); - it = instructions.erase(it); - ++stats.removedSelects; - changed = true; - } + const BlockSet uniform = uniformEntryBlocks(program); + std::uint32_t removed = 1; + while (removed != 0u) { + removed = removeFullWaveSelects(program); + for (const MaskFamily family : {MaskFamily{true, false}, MaskFamily{true, true}, MaskFamily{false, false}, MaskFamily{false, true}}) { + removed += UnreadLanes(program, regions, uniform, family, stats.steps).Remove(); } - if (changed) DeadCodeEliminator{}.Eliminate(program); + stats.removedSelects += removed; + if (removed != 0u) DeadCodeEliminator{}.Eliminate(program); } return stats; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index d5bc4bed1..b73703d71 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -383,7 +383,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. - [sem_timedwait](../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. -- [MaskedSelectEliminator](../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; proofs stop at the existing work and nesting limits. +- [MaskedSelectEliminator](../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; a proof that needs a read mask of a kind that is already being solved is refused. - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. From 6b38cdda8c516184c0d7791be0f6eb2c0856f702 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Mon, 12 Oct 2026 01:53:45 +0530 Subject: [PATCH 300/318] fix(libkernel): report the target size of symbolic links on Windows _wstat64, the FindFirstFileExW listing and std::filesystem::file_size report the size of a file symbolic link itself (0). stat therefore gave 0 for linked files, and APR resolved them as empty, so Unity treated a linked globalgamemanagers as empty and never read it. stat takes the size from the handle it already opens, which follows the link. The APR listing skips reparse points, and names missing from it are sized through stat. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 9 ++++-- .../prx/libkernel/File/src/NativeStat.cpp | 1 + core/libs/tests/GuestAmpr.cpp | 30 +++++++++++++++++++ core/libs/tests/GuestFilesystem.cpp | 7 +++-- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 6910a43eb..631f5553e 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -77,7 +77,7 @@ bool _fileSize(const std::filesystem::path& path, std::uint64_t& bytes) { const HANDLE find = FindFirstFileExW((path.parent_path() / L"*").c_str(), FindExInfoBasic, &entry, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH); if (find != INVALID_HANDLE_VALUE) { do { - if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) continue; + if ((entry.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0) continue; const auto size = (static_cast(entry.nFileSizeHigh) << 32u) | entry.nFileSizeLow; sizes.emplace(_foldCase(std::filesystem::path(entry.cFileName).string()), size); } while (FindNextFileW(find, &entry)); @@ -101,8 +101,11 @@ bool _fileSize(const std::filesystem::path& path, std::uint64_t& bytes) { } // Not in the listing (created since, or an unusual name): ask the file system directly. std::error_code error; - bytes = std::filesystem::file_size(path, error); - return !error; + if (!std::filesystem::is_regular_file(path, error)) return false; + FileStat stat{}; + File::FillFileStat(path, &stat); + bytes = static_cast(stat.st_size); + return true; } int _fail(int guestErrno) { diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 5454db47d..4495fb59f 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -68,6 +68,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { sb->st_mode = static_cast(st.st_mode); sb->st_size = static_cast(st.st_size); #ifdef _WIN32 + if (st.hasIdentity) sb->st_size = static_cast((static_cast(st.identity.nFileSizeHigh) << 32u) | st.identity.nFileSizeLow); sb->st_dev = static_cast(st.st_dev); sb->st_ino = static_cast(st.st_ino); sb->st_nlink = static_cast(st.st_nlink); diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 3b366edd4..37bc4c9df 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -9,12 +9,16 @@ #include #include #include +#include #include #include #include #include #include #include +#ifdef _WIN32 +#include +#endif extern "C" { int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject*); @@ -671,6 +675,31 @@ void TestMalformedCommandSizes() { Require(rejected); } +void TestResolveSymbolicLink() { + const std::filesystem::path directory = "ampr_resolve_links"; + std::filesystem::remove_all(directory); + Require(std::filesystem::create_directory(directory)); + { std::ofstream file(directory / "target.bin", std::ios::binary); file << "linked data"; } + const auto link = directory / "link.bin"; +#ifdef _WIN32 + if (CreateSymbolicLinkW(link.c_str(), L"target.bin", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "APR symbolic link check skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + std::filesystem::remove_all(directory); + return; + } +#else + std::filesystem::create_symlink("target.bin", link); +#endif + const std::string linkPath = link.generic_string(); + const char* path = linkPath.c_str(); + std::uint32_t id = 0; + std::uint64_t size = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIdsAndFileSizes(&path, 1, &id, &size, &failed) == 0 && size == 11); + std::filesystem::remove_all(directory); +} + void TestGatherScatter() { const char* path = "ampr_gather_scatter.bin"; { @@ -1230,6 +1259,7 @@ int main() { TestZeroFilledBuffer(); TestResolveFailures(); TestMalformedCommandSizes(); + TestResolveSymbolicLink(); TestGatherScatter(); TestAmmRangesAboveHintedMaps(); TestAmm(); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index f759e12d3..f531c7ba4 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -428,7 +428,7 @@ int main() { std::filesystem::create_symlink("present.txt", link); Require(lstat_nid_postfix(link.string().c_str(), &linkStatus) == 0); Require((linkStatus.st_mode & 0170000) == 0120000 && linkStatus.st_size == 11); - Require(stat_nid_postfix(link.string().c_str(), &status) == 0 && (status.st_mode & 0170000) == 0100000); + Require(stat_nid_postfix(link.string().c_str(), &status) == 0 && (status.st_mode & 0170000) == 0100000 && status.st_size == 5); const auto dangling = root / "dangling"; std::filesystem::create_symlink("missing.txt", dangling); Require(lstat_nid_postfix(dangling.string().c_str(), &linkStatus) == 0 && (linkStatus.st_mode & 0170000) == 0120000); @@ -443,7 +443,7 @@ int main() { Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0); #else const auto fileLink = root / "file-link"; - if (CreateSymbolicLinkW(fileLink.c_str(), present.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + if (CreateSymbolicLinkW(fileLink.c_str(), L"present.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); std::fprintf(stderr, "Windows lstat link checks skipped: creating symbolic links needs Developer Mode or administrator rights\n"); } else { @@ -451,7 +451,7 @@ int main() { Require(std::filesystem::create_directory(targetDirectory)); const auto directoryLink = root / "directory-link"; const auto danglingLink = root / "dangling-link"; - Require(CreateSymbolicLinkW(directoryLink.c_str(), targetDirectory.c_str(), + Require(CreateSymbolicLinkW(directoryLink.c_str(), L"target-directory", SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); Require(CreateSymbolicLinkW(danglingLink.c_str(), missing.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); const auto checkUnsupportedLink = [](const std::filesystem::path& link) { @@ -470,6 +470,7 @@ int main() { const bool statusUnchanged = std::all_of(bytes, bytes + sizeof(untouched), [](unsigned char value) { return value == 0x5a; }); return rejected && errorUnchanged && statusUnchanged; }; + Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); Require(checkUnsupportedLink(fileLink)); Require(checkUnsupportedLink(directoryLink)); Require(checkUnsupportedLink(danglingLink)); From 9f061763346787806990aabb57b1a1bfb69860ce Mon Sep 17 00:00:00 2001 From: Swastik-51 Date: Fri, 9 Oct 2026 01:27:29 +0530 Subject: [PATCH 301/318] fix(agc): cache Linux describePages results --- .../Execution/src/GuestMemory.cpp | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index de2fb32eb..77e3ac3e9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -423,6 +423,17 @@ struct PageStates { arena.forget(address, bytes); image.forget(address, bytes); } + + void record(std::uintptr_t base, std::uintptr_t regionEnd, bool readable, bool writable, std::uint64_t generation) { + for (PageSpan* span : {&arena, &image}) { + if (!readable || span->size == 0 || regionEnd <= span->base || base >= span->base + span->size) continue; + const std::uint8_t value = PageReadable | (writable ? PageWritable : 0u); + const auto first = std::max(base, span->base); + const auto last = std::min(regionEnd, span->base + span->size); + for (auto at = first; at < last; at += PageBytes) span->store(at, value); + if (GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) span->forget(first, static_cast(last - first)); + } + } }; PageStates& Pages() { @@ -533,18 +544,12 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { const bool committed = memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0; const bool readable = committed && readableProtection(protection); const bool writable = readable && writableProtection(protection); - for (PageSpan* span : {&pages.arena, &pages.image}) { - if (!readable || span->size == 0 || regionEnd <= span->base || base >= span->base + span->size) continue; - const std::uint8_t value = PageReadable | (writable ? PageWritable : 0u); - const auto first = std::max(base, span->base); - const auto last = std::min(regionEnd, span->base + span->size); - for (auto at = first; at < last; at += PageBytes) span->store(at, value); - if (GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) span->forget(first, static_cast(last - first)); - } + pages.record(base, regionEnd, readable, writable, generation); const auto next = std::min(end, regionEnd); if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; #else + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); if (const int fd = HostMapping::QueryFd(); fd >= 0) { procmap_query query{}; query.size = sizeof(query); @@ -558,9 +563,13 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { cursor = gapEnd; continue; } + const auto base = static_cast(query.vma_start); + const auto regionEnd = static_cast(query.vma_end); const bool readable = (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) != 0; + const bool writable = readable && (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0; + pages.record(base, regionEnd, readable, writable, generation); const auto next = std::min(end, query.vma_end); - if (!emit(PageRun{cursor, next, readable, readable && (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0})) return true; + if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; continue; } @@ -589,8 +598,11 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { found = true; break; } + const bool readable = permissions[0] == 'r'; + const bool writable = readable && permissions[1] == 'w'; + pages.record(first, last, readable, writable, generation); const auto next = std::min(end, last); - if (!emit(PageRun{cursor, next, permissions[0] == 'r', permissions[0] == 'r' && permissions[1] == 'w'})) return true; + if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; found = true; break; From 49e9298706212d0972197d09d7434f0113224b06 Mon Sep 17 00:00:00 2001 From: Ahir <191682168+ahir061@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:00:05 +0530 Subject: [PATCH 302/318] fix(agc): store only the requested pages of a Linux page query describePages stored the whole VMA that PROCMAP_QUERY or /proc/self/maps returned. Linux merges adjacent anonymous mappings with equal flags, so an arena VMA can span GiBs, and the first table miss inside it after an invalidation rewrote every page of it. Only the pages of the request, rounded out to whole pages, are stored now, as #1791 describes. When the guest arena is available, RunUnmappedGapTests checks that a check stores no pages outside its range, and that protecting and unmapping through the registry clears the stored pages. The registry checks follow the test on the perf/linux-page-cache branch linked from #1791. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/GuestMemory.cpp | 10 ++++--- .../tests/GuestAllocations.cpp | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 77e3ac3e9..b884243b9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -550,6 +550,10 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { cursor = next; #else const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + const auto recordRun = [&](std::uintptr_t next, bool readable, bool writable) { + constexpr auto mask = static_cast(PageBytes - 1); + pages.record(cursor & ~mask, (next + mask) & ~mask, readable, writable, generation); + }; if (const int fd = HostMapping::QueryFd(); fd >= 0) { procmap_query query{}; query.size = sizeof(query); @@ -563,12 +567,10 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { cursor = gapEnd; continue; } - const auto base = static_cast(query.vma_start); - const auto regionEnd = static_cast(query.vma_end); const bool readable = (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) != 0; const bool writable = readable && (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0; - pages.record(base, regionEnd, readable, writable, generation); const auto next = std::min(end, query.vma_end); + recordRun(next, readable, writable); if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; continue; @@ -600,8 +602,8 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { } const bool readable = permissions[0] == 'r'; const bool writable = readable && permissions[1] == 'w'; - pages.record(first, last, readable, writable, generation); const auto next = std::min(end, last); + recordRun(next, readable, writable); if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; found = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index f4f02db6a..de79f6a57 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -1,5 +1,6 @@ #include "BdaTests.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestHeap.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" @@ -187,5 +188,33 @@ void RunUnmappedGapTests() { Require(!GuestMemory::Accessible(reinterpret_cast(std::uintptr_t{0x18}), 4), "a near-null address counts as accessible"); munmap(block, page); munmap(block + 2 * page, page); + if (!GuestArena::GuestArenaAvailable_nid_postfix()) return; + auto* guest = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * page, page)); + Require(mmap(guest, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == guest, "cannot map the arena page test pages"); + { + GuestAllocations::Mutation mutation; + mutation.Add(guest, 2 * page, true, true); + } + Require(GuestMemory::Accessible(guest, page, true), "a mapped arena page is not writable"); + Require(mprotect(guest + page, page, PROT_READ) == 0, "cannot protect the arena test's second page"); + Require(!GuestMemory::Accessible(guest + page, page, true), "a check stored arena pages outside its range"); + { + GuestAllocations::Mutation mutation; + mutation.Protect(guest + page, page, true, true, [&] { Require(mprotect(guest + page, page, PROT_READ | PROT_WRITE) == 0, "cannot restore the arena test's second page"); }); + } + Require(GuestMemory::Accessible(guest, 2 * page, true), "mapped arena pages are not writable"); + { + GuestAllocations::Mutation mutation; + mutation.Protect(guest, page, true, false, [&] { Require(mprotect(guest, page, PROT_READ) == 0, "cannot protect the arena test page"); }); + } + Require(!GuestMemory::Accessible(guest, page, true) && GuestMemory::Accessible(guest, page) && GuestMemory::Accessible(guest + page, page, true), "an arena page protected through the registry kept its old access"); + { + GuestAllocations::Mutation mutation; + const auto release = [](const void* pointer, std::size_t bytes, const void*, bool) { Require(munmap(const_cast(pointer), bytes) == 0, "cannot unmap an arena test page"); }; + mutation.Unmap(guest, page, release); + mutation.Unmap(guest + page, page, release); + } + Require(!GuestMemory::Accessible(guest, page) && !GuestMemory::Accessible(guest + page, page), "unmapped arena pages are still accessible"); + GuestArena::GuestArenaRelease_nid_postfix(guest, 2 * page); #endif } From d635d1896761fc71b2dedfdf2df047d1bca8a5c1 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sat, 10 Oct 2026 23:00:41 +1300 Subject: [PATCH 303/318] test(agc): check that a Linux page query is answered from the page table Revoke a checked arena page with mprotect behind the registry and check that the stored answer comes back, so the test fails without the cache. Pass the gpu argument that GuestAllocations::Mutation::Add and Protect now take. --- core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index de79f6a57..ad2314a04 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -193,19 +193,22 @@ void RunUnmappedGapTests() { Require(mmap(guest, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == guest, "cannot map the arena page test pages"); { GuestAllocations::Mutation mutation; - mutation.Add(guest, 2 * page, true, true); + mutation.Add(guest, 2 * page, true, true, true); } Require(GuestMemory::Accessible(guest, page, true), "a mapped arena page is not writable"); + Require(mprotect(guest, page, PROT_NONE) == 0, "cannot revoke the arena test page behind the registry"); + Require(GuestMemory::Accessible(guest, page, true), "a checked arena page was queried again instead of read from the page table"); + Require(mprotect(guest, page, PROT_READ | PROT_WRITE) == 0, "cannot restore the arena test page behind the registry"); Require(mprotect(guest + page, page, PROT_READ) == 0, "cannot protect the arena test's second page"); Require(!GuestMemory::Accessible(guest + page, page, true), "a check stored arena pages outside its range"); { GuestAllocations::Mutation mutation; - mutation.Protect(guest + page, page, true, true, [&] { Require(mprotect(guest + page, page, PROT_READ | PROT_WRITE) == 0, "cannot restore the arena test's second page"); }); + mutation.Protect(guest + page, page, true, true, true, [&] { Require(mprotect(guest + page, page, PROT_READ | PROT_WRITE) == 0, "cannot restore the arena test's second page"); }); } Require(GuestMemory::Accessible(guest, 2 * page, true), "mapped arena pages are not writable"); { GuestAllocations::Mutation mutation; - mutation.Protect(guest, page, true, false, [&] { Require(mprotect(guest, page, PROT_READ) == 0, "cannot protect the arena test page"); }); + mutation.Protect(guest, page, true, false, true, [&] { Require(mprotect(guest, page, PROT_READ) == 0, "cannot protect the arena test page"); }); } Require(!GuestMemory::Accessible(guest, page, true) && GuestMemory::Accessible(guest, page) && GuestMemory::Accessible(guest + page, page, true), "an arena page protected through the registry kept its old access"); { From 2c6e3f53ffea56b180f693f38fb9cc7e626220ef Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 14:06:37 +0200 Subject: [PATCH 304/318] feat(libc): implement sinhf Ruffle Flash 1.2.0 (PPSA68091) stops on main 0f863c10 at FAIL: unresolved ELF import 1t1-JoZ0sZQ from libSceLibcInternal.prx 1t1-JoZ0sZQ is sinhf (tools/nid_names.py, aerolib). RetroArch 1.0.0-beta.1 (PPSA99169) imports it as well. The implementation follows the sinhf of the libc.prx bundled with PPSA28319, read from its disassembly: FreeBSD msun's e_sinhf.c as of 5aa554c7 (before __ldexp_expf, so expf(|x| / 2) is squared between logf(FLT_MAX) and the overflow threshold), calling msun stable/9 expm1f and expf (59bc2f09), whose paths for the arguments sinhf passes are inlined. Past the threshold Sony's error helper sets errno to ERANGE before returning x * 1e37; NaN and the infinities return x + x and leave errno alone. Running that code from the title's libc.prx on a Windows host gives the same bits as this implementation for every finite argument up to the threshold, both signs (2,238,032,378 inputs). guest_math pins a set of those bits, the overflow errno and FE_OVERFLOW, and checks that errno is untouched otherwise. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/src/Math.cpp | 75 +++++++++++++++++++++++++++++++++ core/libs/tests/GuestMath.cpp | 34 +++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 110 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index b229fd67a..d7ae1f65e 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -24,6 +24,60 @@ std::int64_t RoundToInt64(TFloat value) { return static_cast(rounded); } +float SinhfExpm1(float x) { + constexpr float Ln2High = 6.9313812256e-01f, Ln2Low = 9.0580006145e-06f, InverseLn2 = 1.4426950216e+00f; + constexpr float Q1 = -3.3333212137e-2f, Q2 = 1.5807170421e-3f; + const std::uint32_t magnitude = std::bit_cast(x); + int k = 0; + float c = 0.0f; + if (magnitude > 0x3eb17218u) { + float high; + float low; + if (magnitude < 0x3f851592u) { + high = x - Ln2High; + low = Ln2Low; + k = 1; + } else { + k = static_cast(InverseLn2 * x + 0.5f); + const float t = static_cast(k); + high = x - t * Ln2High; + low = t * Ln2Low; + } + x = high - low; + c = (high - x) - low; + } + const float halfX = 0.5f * x; + const float halfSquare = x * halfX; + const float r1 = 1.0f + halfSquare * (Q1 + halfSquare * Q2); + const float t = 3.0f - r1 * halfX; + float e = halfSquare * ((r1 - t) / (6.0f - x * t)); + if (k == 0) return x - (x * e - halfSquare); + e = x * (e - c) - c; + e -= halfSquare; + if (k == 1) { + if (x < -0.25f) return -2.0f * (e - (x + 0.5f)); + return 1.0f + 2.0f * (x - e); + } + const float twoPowK = std::bit_cast(static_cast(0x7f + k) << 23); + const float oneMinusTwoPowMinusK = std::bit_cast(0x3f800000u - (0x1000000u >> k)); + return (oneMinusTwoPowMinusK - (e - x)) * twoPowK; +} + +float SinhfExp(float x) { + constexpr float Ln2High = 6.9314575195e-01f, Ln2Low = 1.4286067653e-06f, InverseLn2 = 1.4426950216e+00f; + constexpr float P1 = 1.6666625440e-1f, P2 = -2.7667332906e-3f; + const int k = static_cast(InverseLn2 * x + 0.5f); + const float kFloat = static_cast(k); + const float high = x - kFloat * Ln2High; + const float low = kFloat * Ln2Low; + x = high - low; + const float t = x * x; + const float c = x - t * (P1 + t * P2); + const float y = 1.0f - ((low - (x * c) / (2.0f - c)) - high); + if (k == 128) return y * 2.0f * 0x1p127f; + return y * std::bit_cast(static_cast(0x7f + k) << 23); +} + } extern "C" { @@ -119,6 +173,27 @@ double APS5_VABI cos_nid_postfix(double x) { return std::cos(x); } double APS5_VABI sinh_nid_postfix(double x) { return std::sinh(x); } double APS5_VABI cosh_nid_postfix(double x) { return std::cosh(x); } float APS5_VABI coshf_nid_postfix(float x) { return std::cosh(x); } + +float APS5_VABI sinhf_nid_postfix(float x) { + const std::uint32_t magnitude = std::bit_cast(x) & 0x7fffffffu; + if (magnitude >= 0x7f800000u) return x + x; + const float half = std::signbit(x) ? -0.5f : 0.5f; + const float absolute = std::fabs(x); + if (magnitude < 0x41100000u) { + if (magnitude < 0x39800000u && 1.0e37f + x > 1.0f) return x; + const float t = SinhfExpm1(absolute); + if (magnitude < 0x3f800000u) return half * (2.0f * t - t * t / (t + 1.0f)); + return half * (t + t / (t + 1.0f)); + } + if (magnitude < 0x42b17217u) return half * SinhfExp(absolute); + if (magnitude <= 0x42b2d4fcu) { + const float w = SinhfExp(0.5f * absolute); + return half * w * w; + } + errno = ERANGE; + return x * 1.0e37f; +} + double APS5_VABI asinh_nid_postfix(double x) { return std::asinh(x); } float APS5_VABI asinhf_nid_postfix(float x) { return std::asinh(x); } double APS5_VABI acosh_nid_postfix(double x) { return std::acosh(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 298b45c48..95ae0fe77 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -69,6 +69,7 @@ std::div_t APS5_VABI div_nid_postfix(int, int); double APS5_VABI sinh_nid_postfix(double); double APS5_VABI cosh_nid_postfix(double); float APS5_VABI coshf_nid_postfix(float); +float APS5_VABI sinhf_nid_postfix(float); double APS5_VABI asinh_nid_postfix(double); float APS5_VABI asinhf_nid_postfix(float); double APS5_VABI acosh_nid_postfix(double); @@ -401,6 +402,38 @@ static void CheckHyperbolic() { Require(Near(acosh_nid_postfix(1e300), 691.4686750787736, 1e-15)); } +static void CheckSinhf() { + const float infinity = std::numeric_limits::infinity(); + const struct { std::uint32_t input; std::uint32_t output; } cases[] = { + {0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u}, {0x00000001u, 0x00000001u}, {0x39000000u, 0x39000000u}, + {0x3e800000u, 0x3e815667u}, {0xbf000000u, 0xbf056680u}, {0x3f800000u, 0x3f966cfeu}, {0xbf800000u, 0xbf966cfeu}, + {0x40000000u, 0x40681e7bu}, {0x41100000u, 0x457d38acu}, {0xc1a00000u, 0xcd675844u}, {0x42b17217u, 0x7effff84u}, + {0x42b20000u, 0x7f28e166u}, {0xc2b20000u, 0xff28e166u}, {0x42b2d4fcu, 0x7f7fffecu}, + {0x7f800000u, 0x7f800000u}, {0xff800000u, 0xff800000u}, + }; + for (const auto& test : cases) { + *__error_nid_postfix() = 12345; + const float result = sinhf_nid_postfix(std::bit_cast(test.input)); + if (std::bit_cast(result) != test.output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest sinhf failed for %08x\n", test.input); + std::abort(); + } + } + Require(std::isnan(sinhf_nid_postfix(std::numeric_limits::quiet_NaN())) && *__error_nid_postfix() == 12345); + Require(Near(sinhf_nid_postfix(1.f), 1.1752011936438014, 1e-6) && Near(sinhf_nid_postfix(-2.f), -3.626860407847019, 1e-6)); + Require(Near(sinhf_nid_postfix(0.25f), 0.25261231680816831, 1e-6) && Near(sinhf_nid_postfix(20.f), 242582597.70489514, 1e-6)); + for (const std::uint32_t input : {0x42b2d4fdu, 0x42c80000u, 0x7f7fffffu}) { + for (const std::uint32_t sign : {0u, 0x80000000u}) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + const float result = sinhf_nid_postfix(std::bit_cast(input | sign)); + Require(result == (sign != 0 ? -infinity : infinity) && *__error_nid_postfix() == ERANGE); + Require(std::fetestexcept(FE_OVERFLOW) != 0); + } + } + *__error_nid_postfix() = 0; +} + static void CheckFloatClassification() { Require(_FInf_nid_postfix.bits[0] == 0x7f800000u && _FNan_nid_postfix.bits[0] == 0x7fc00000u); for (int word = 1; word < 4; ++word) Require(_FInf_nid_postfix.bits[word] == 0 && _FNan_nid_postfix.bits[word] == 0); @@ -624,6 +657,7 @@ int main() { Require(std::isinf(__powisf2_nid_postfix(-0.0f, -3)) && std::signbit(__powisf2_nid_postfix(-0.0f, -3))); Require(__powisf2_nid_postfix(2.0f, -127) == 0x1p-127f); CheckHyperbolic(); + CheckSinhf(); CheckHypotSpecialValue(); CheckFmodErrno(); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 446bdd0bc..96d3d5766 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -431,6 +431,7 @@ Throughout the project, every function at every stage either **does exactly what - [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [Draw input snapshots](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing +- [sinhf](../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: From 5f3fb3dddd1c003704655023cd21e1d27e97c961 Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 14:07:34 +0200 Subject: [PATCH 305/318] feat(libc): implement asinl RetroArch 1.0.0-beta.1 (PPSA99169) stops on main 0f863c10 at FAIL: unresolved ELF import Nx-F5v0-qU8 from libSceLibcInternal.prx Nx-F5v0-qU8 is asinl (tools/nid_names.py, aerolib). The implementation follows the asinl of the libc.prx bundled with PPSA28319, read from its disassembly: FreeBSD msun's e_asinl.c with the ld80 invtrig.c coefficients (9f043204), which the binary holds as the same 80-bit constants, computed in the x87 long double the guest uses. +-1 returns +-pi/2 rounded to 64 bits and +-0 keeps its sign. For |x| > 1, the infinities and NaN, Sony's error helper raises FE_INVALID and sets errno to EDOM before returning (x - x) / (x - x), so a NaN argument sets EDOM too. 20 million random arguments in (-1, 1) plus the branch edges give the same bits as that code from the title's libc.prx run on a Windows host. guest_math pins a set of those bits and checks the domain errors' errno and FE_INVALID, and that errno is untouched otherwise. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/src/Math.cpp | 77 +++++++++++++++++++++++++++++++++ core/libs/tests/GuestMath.cpp | 45 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 123 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index d7ae1f65e..052601ef6 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "prx/libc/include/General.hpp" @@ -78,6 +79,52 @@ float SinhfExp(float x) { return y * std::bit_cast(static_cast(0x7f + k) << 23); } +static_assert(std::numeric_limits::digits == 64, "guest long double is the x87 80-bit format"); + +struct ExtendedParts { + std::uint64_t mantissa; + std::uint16_t signExponent; +}; + +ExtendedParts SplitExtended(long double value) { + unsigned char bytes[sizeof(long double)]; + std::memcpy(bytes, &value, sizeof(value)); + ExtendedParts parts; + std::memcpy(&parts.mantissa, bytes, sizeof(parts.mantissa)); + std::memcpy(&parts.signExponent, bytes + sizeof(parts.mantissa), sizeof(parts.signExponent)); + return parts; +} + +long double ClearLowMantissaWord(long double value) { + unsigned char bytes[sizeof(long double)]; + std::memcpy(bytes, &value, sizeof(value)); + std::memset(bytes, 0, sizeof(std::uint32_t)); + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +constexpr long double AsinNumeratorCoefficients[] = { + 1.66666666666666666631e-01L, -4.16313987993683104320e-01L, 3.69068046323246813704e-01L, -1.36213932016738603108e-01L, + 1.78324189708471965733e-02L, -2.19216428382605211588e-04L, -7.10526623669075243183e-06L, +}; +constexpr long double AsinDenominatorCoefficients[] = { + -2.94788392796209867269e+00L, 3.27309890266528636716e+00L, -1.68285799854822427013e+00L, 3.90699412641738801874e-01L, + -3.14365703596053263322e-02L, +}; +constexpr long double HalfPiHigh = 1.57079632679489661926e+00L; +constexpr long double HalfPiLow = -2.50827880633416601173e-20L; +constexpr long double QuarterPiHigh = 7.85398163397448309628e-01L; + +long double AsinNumerator(long double t) { + const auto& p = AsinNumeratorCoefficients; + return t * (p[0] + t * (p[1] + t * (p[2] + t * (p[3] + t * (p[4] + t * (p[5] + t * p[6])))))); +} + +long double AsinDenominator(long double t) { + const auto& q = AsinDenominatorCoefficients; + return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4])))); +} + } extern "C" { @@ -147,6 +194,36 @@ double APS5_VABI trunc_nid_postfix(double x) { return std::trunc(x); } double APS5_VABI fma_nid_postfix(double x, double y, double z) { return std::fma(x, y, z); } long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); } +long double APS5_VABI asinl_nid_postfix(long double x) { + const ExtendedParts parts = SplitExtended(x); + const int exponent = parts.signExponent & 0x7fff; + if (exponent >= 0x3fff) { + if (exponent == 0x3fff && (parts.mantissa & 0x7fffffffffffffffull) == 0) return x * HalfPiHigh + x * HalfPiLow; + std::feraiseexcept(FE_INVALID); + errno = EDOM; + return (x - x) / (x - x); + } + if (exponent < 0x3ffe) { + if (exponent < 0x3fff - 32 && 1.0e300L + x > 1.0L) return x; + const long double t = x * x; + return x + x * (AsinNumerator(t) / AsinDenominator(t)); + } + const long double t = (1.0L - std::fabs(x)) * 0.5L; + const long double p = AsinNumerator(t); + const long double q = AsinDenominator(t); + const long double s = std::sqrt(t); + long double result; + if ((parts.mantissa >> 32) >= 0xf3333333u) { + result = HalfPiHigh - (2.0L * (s + s * (p / q)) - HalfPiLow); + } else { + const long double w = ClearLowMantissaWord(s); + const long double c = (t - w * w) / (s + w); + const long double r = p / q; + result = QuarterPiHigh - ((2.0L * s * r - (HalfPiLow - 2.0L * c)) - (QuarterPiHigh - 2.0L * w)); + } + return (parts.signExponent & 0x8000) != 0 ? -result : result; +} + long double APS5_VABI frexpl_nid_postfix(long double x, int* exponent) { if (!std::isfinite(x)) return x; return std::frexp(x, exponent); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 95ae0fe77..ee817d3ad 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -31,6 +31,7 @@ int APS5_VABI abs_nid_postfix(int); double APS5_VABI trunc_nid_postfix(double); double APS5_VABI fma_nid_postfix(double, double, double); long double APS5_VABI acosl_nid_postfix(long double); +long double APS5_VABI asinl_nid_postfix(long double); long double APS5_VABI frexpl_nid_postfix(long double, int*); struct LibcFloatConstant { std::uint32_t bits[4]; }; extern LibcFloatConstant _FInf_nid_postfix; @@ -185,6 +186,49 @@ static void CheckFmodErrno() { Require(fmod_nid_postfix(1e300, 3.0) == std::fmod(1e300, 3.0)); } +static long double MakeExtended(std::uint64_t mantissa, std::uint16_t signExponent) { + unsigned char bytes[sizeof(long double)]{}; + std::memcpy(bytes, &mantissa, 8); + std::memcpy(bytes + 8, &signExponent, 2); + long double value = 0; + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +static void CheckAsinl() { + const struct { std::uint64_t inputMantissa; std::uint16_t inputSignExponent; std::uint64_t outputMantissa; std::uint16_t outputSignExponent; } cases[] = { + {0, 0x0000, 0, 0x0000}, {0, 0x8000, 0, 0x8000}, + {0x8000000000000000ull, 0x3fd7, 0x8000000000000000ull, 0x3fd7}, {0x8000000000000000ull, 0xbfd7, 0x8000000000000000ull, 0xbfd7}, + {0x8000000000000000ull, 0x3ffd, 0x815f4e705c3e68f3ull, 0x3ffd}, {0x8000000000000000ull, 0xbffd, 0x815f4e705c3e68f3ull, 0xbffd}, + {0xcccccccccccccccdull, 0x3ffb, 0xcd2493b01bc41e02ull, 0x3ffb}, + {0x8000000000000000ull, 0x3ffe, 0x860a91c16b9b2c23ull, 0x3ffe}, {0x8000000000000000ull, 0xbffe, 0x860a91c16b9b2c23ull, 0xbffe}, + {0xc000000000000000ull, 0xbffe, 0xd91a98ae3406e041ull, 0xbffe}, {0xe666666666666666ull, 0x3ffe, 0x8f549b82fd0b8fa3ull, 0x3fff}, + {0xf333333333333333ull, 0x3ffe, 0xa06a08acf9a04f78ull, 0x3fff}, {0xfd70a3d70a3d70a4ull, 0xbffe, 0xb6f1e3799ea8ecc3ull, 0xbfff}, + {0xffffffffffffffffull, 0x3ffe, 0xc90fdaa16c63cf01ull, 0x3fff}, + {0x8000000000000000ull, 0x3fff, 0xc90fdaa22168c235ull, 0x3fff}, {0x8000000000000000ull, 0xbfff, 0xc90fdaa22168c235ull, 0xbfff}, + }; + for (const auto& test : cases) { + *__error_nid_postfix() = 12345; + const long double result = asinl_nid_postfix(MakeExtended(test.inputMantissa, test.inputSignExponent)); + if (!ExtendedBits(result, test.outputMantissa, test.outputSignExponent) || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest asinl failed for %016llx:%04x\n", static_cast(test.inputMantissa), test.inputSignExponent); + std::abort(); + } + } + Require(std::fabs(asinl_nid_postfix(0.5L) - 0.523598775598298873077107230547L) <= 0x1p-64L); + const long double domainErrors[] = { + 1.5L, -2.0L, MakeExtended(0x8000000000000001ull, 0x3fff), MakeExtended(0x8000000000000001ull, 0xbfff), + std::numeric_limits::max(), std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN(), + }; + for (const long double input : domainErrors) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + Require(std::isnan(asinl_nid_postfix(input)) && *__error_nid_postfix() == EDOM && std::fetestexcept(FE_INVALID) != 0); + } + *__error_nid_postfix() = 0; +} + static void CheckIntegerConversions() { for (const long long numerator : {4294967301LL, -4294967301LL}) { for (const long long denominator : {3LL, -3LL}) { @@ -575,6 +619,7 @@ int main() { CheckSquareRoot(); CheckExactOperations(); CheckLongDouble(); + CheckAsinl(); Require(scalbn_nid_postfix(0.75, -2) == 0.1875); Require(scalbnf_nid_postfix(0.75f, 4) == 12.f); Require(nextafterf_nid_postfix(1.f, 2.f) == 0x1.000002p0f && nextafterf_nid_postfix(1.f, 0.f) == 0x1.fffffep-1f); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 96d3d5766..8a36c75f8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -432,6 +432,7 @@ Throughout the project, every function at every stage either **does exactly what - [Draw input snapshots](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [sinhf](../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed +- [asinl](../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: From 295e6b501d96d04cccb33149b0e59b5667935aad Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 23:31:40 +0000 Subject: [PATCH 306/318] feat(libSceNpAuthAuthorizedAppDialog): add the dialog, with NpAuth and SaveData imports of Minecraft Minecraft (PPSA17221) fails to load on nine absent imports. The authorized-app dialog runs the common-dialog state machine without UI and reports the user as having cancelled, like the login and VR setup dialogs. The other two, sceNpAuthGetAuthorizedAppCode and the unnamed libSceSaveData.native NID RvID8BVeg+g, are declared as throwing stubs with unknown signatures. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceNpAuth/Export.cpp | 5 ++ .../CMakeLists.txt | 14 ++++ .../Export.cpp | 70 +++++++++++++++++++ .../libs/prx/libSceSaveData.native/Export.cpp | 7 ++ .../tests/GuestNpAuthAuthorizedAppDialog.cpp | 62 ++++++++++++++++ docs/dev/TechnicalDebt.md | 3 + 6 files changed, 161 insertions(+) create mode 100644 core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt create mode 100644 core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp create mode 100644 core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 1ef3f03a6..c17210d9f 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -32,6 +32,11 @@ int APS5_VABI sceNpAuthDeleteRequest(int req_id) { return 0; } +int APS5_VABI sceNpAuthGetAuthorizedAppCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id) { (void)req_id; (void)issuer_id; diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt new file mode 100644 index 000000000..3d65cfe79 --- /dev/null +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceNpAuthAuthorizedAppDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpAuthAuthorizedAppDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpAuthAuthorizedAppDialog) + +if(BUILD_TESTING) + add_test_executable(guest_np_auth_authorized_app_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpAuthAuthorizedAppDialog.cpp) + target_include_directories(guest_np_auth_authorized_app_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_auth_authorized_app_dialog_tests PRIVATE libSceNpAuthAuthorizedAppDialog) + add_test(NAME guest_np_auth_authorized_app_dialog COMMAND guest_np_auth_authorized_app_dialog_tests) + if(WIN32) + set_tests_properties(guest_np_auth_authorized_app_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp new file mode 100644 index 000000000..6a722de5a --- /dev/null +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + +extern "C" { + +int APS5_VABI sceNpAuthAuthorizedAppDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); + return 0; +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; +} + +} diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 51688fbc5..0be226642 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -808,4 +808,11 @@ int APS5_VABI sceSaveDataDirNameSearch2() { NotImplemented_nid_no_patch(__func__); return 0; } + +APS5_EXPORT("RvID8BVeg+g", sceSaveDataUnknown_RvID8BVeg); +int APS5_VABI sceSaveDataUnknown_RvID8BVeg() { + NotImplemented_nid_no_patch("RvID8BVeg+g"); + return 0; +} + } diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp new file mode 100644 index 000000000..e8a0a3afc --- /dev/null +++ b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp @@ -0,0 +1,62 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceNpAuthAuthorizedAppDialogInitialize(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const void* param); +int APS5_VABI sceNpAuthAuthorizedAppDialogUpdateStatus(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result); +int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotInitialized); + + Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); + Require(sceNpAuthAuthorizedAppDialogInitialize() == kErrAlreadyInitialized); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusInitialized); + + Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotFinished); + + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == 0); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusFinished); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusFinished); + + Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceNpAuthAuthorizedAppDialogClose() == 0); + Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); + Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 446bdd0bc..6c9e26b72 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -18,6 +18,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceErrorDialog](../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged - [libSceLoginDialog](../../core/libs/prx/libSceLoginDialog/Export.cpp) - the login dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled - [libSceCdlgG2P](../../core/libs/prx/libSceCdlgG2P/Export.cpp) - the G2P dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. The signatures (`Open` and `GetResult` take a pointer, the others none) and the leading `int32` result are assumed from the other common dialogs, not verified for G2P; PPSA01289 imports it +- [libSceNpAuthAuthorizedAppDialog](../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - the authorized-app dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN sign-in to authorize an app against. The signatures follow the other common dialogs (`Open` takes a parameter block, `GetResult` writes a 32-bit result); Minecraft (PPSA17221) imports the seven functions and fails to load without them. - [libSceVrSetupDialog](../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are assumed from the other common dialogs; PPSA12544 imports it - [scePthreadMutexattrSetprotocol](../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. @@ -59,6 +60,8 @@ Throughout the project, every function at every stage either **does exactly what - [nearbyintf](../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. - [_Thrd_sleep](../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [sceCoredumpGetThreadContextInfo](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it +- [sceNpAuthGetAuthorizedAppCode](../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it and fails to load without it. The request parameter and code output are not known, and there is no PSN sign-in for it to succeed against. +- [sceSaveDataUnknown_RvID8BVeg](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. No name in aerolib or among the PS4-migration variants tried with `tools/nid_names.py` hashes to it. - [sceNpWebApi2PushEventAbortHandle](../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. - [sceNpGetSandboxInfo](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [sceKernelGetSystemSwVersion](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000). The 0x28-byte layout (`uint64_t size` at 0, `versionString` at 8 and `version` at 0x24) matches [ProsperoStore's caller](https://github.com/blackbearreloaded/ProsperoStore/blob/20584011/src/diag/trace.cpp#L20-L26), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d1/src/core/libraries/kernel/kernel.h#L45-L49), [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6/kernel/ps4_libkernel.pas#L502-L506) and [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4/include/orbis/_types/kernel.h#L256-L260). These sources support the structure layout, but not the reported version value: the console firmware version remains unverified. The checks are unverified too: a null pointer returns `SCE_KERNEL_ERROR_EFAULT` and a `size` other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL`, while shadPS4 returns success for null and never reads `size`. From 7949388c0bee389b7f0850644e9407ade95b876b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 11:05:40 +0200 Subject: [PATCH 307/318] docs(contributing): name the title a pull request unblocks Co-Authored-By: Claude --- .github/pull_request_template.md | 1 + CONTRIBUTING.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fec7af47a..94ee308e6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,4 +13,5 @@ - [ ] New third-party code is a submodule built from source - [ ] Generic behaviour, not specific to one title - [ ] Depends on: +- [ ] Unblocks: - [ ] AI-assisted: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 900ea736e..cc7c0ec8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,9 @@ The [pull request template](.github/pull_request_template.md) is the checklist f - Before starting, check that no open pull request already implements the same functions. - Keep the branch up to date with `main` and resolve conflicts yourself; rebasing and force-pushing is fine. - If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise). +- If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first. +- Fixes a title needs go to `main` as pull requests. A Game bring-up discussion lists the open pull requests and local changes the title still needs; update the list when one is merged, closed or replaced. +- The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`. - Run the tests before opening the pull request and describe what was tested (OS, title or homebrew). - Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments. - Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it. From d9d00f0e3420f2816846f3d43f369ea8158cb2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 12 Oct 2026 11:35:11 +0200 Subject: [PATCH 308/318] ci(build): run pull request jobs only when their files change Co-Authored-By: Claude --- .github/workflows/build.yml | 26 ++++- CMakeLists.txt | 2 + CONTRIBUTING.md | 2 +- tools/check_conventions.py | 59 ++++++++++ tools/tests/test_check_conventions_ci_gate.py | 103 ++++++++++++++++++ 5 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 tools/tests/test_check_conventions_ci_gate.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 724a3cfe0..c32d15923 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,8 @@ jobs: pr: ${{ steps.gate.outputs.pr }} head: ${{ steps.gate.outputs.head }} relinker: ${{ steps.gate.outputs.relinker }} + register-context: ${{ steps.gate.outputs.register-context }} + write-watch: ${{ steps.gate.outputs.write-watch }} full: ${{ steps.gate.outputs.full }} steps: - uses: actions/github-script@v8 @@ -55,6 +57,8 @@ jobs: core.setOutput('pr', ''); core.setOutput('head', ''); core.setOutput('relinker', true); + core.setOutput('register-context', true); + core.setOutput('write-watch', true); core.setOutput('full', true); await core.summary.addRaw(`Building main at ${context.sha}: all builds and tests enabled`).write(); return; @@ -97,16 +101,32 @@ jobs: }); relinker = commits.length > 0; } + const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); + const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)); + const touches = (...prefixes) => manual || files.length >= 3000 || + paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); + const workflow = '.github/workflows/build.yml'; + const libc = 'core/libs/prx/libc/'; + relinker = relinker && touches( + 'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp', + 'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp', + 'tools/', 'cmake/', 'CMakeLists.txt', workflow + ); + const registerContext = touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow); + const writeWatch = touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow); const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' }); core.setOutput('sha', main.commit.sha); core.setOutput('pr', String(pull_number)); core.setOutput('head', pr.head.sha); core.setOutput('relinker', relinker); + core.setOutput('register-context', registerContext); + core.setOutput('write-watch', writeWatch); core.setOutput('full', manual); core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`); await core.summary.addRaw( `PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` + - `relinker ${relinker ? 'enabled' : 'skipped because the author has no commit in main'}` + `relinker ${relinker ? 'enabled' : 'skipped: no relinker file changed or the author has no commit in main'}, ` + + `register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}` ).write(); relinker: @@ -162,6 +182,8 @@ jobs: - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 register-context: + needs: eligibility + if: ${{ needs.eligibility.outputs.register-context == 'true' }} strategy: fail-fast: false matrix: @@ -176,6 +198,8 @@ jobs: - run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error write-watch: + needs: eligibility + if: ${{ needs.eligibility.outputs.write-watch == 'true' }} strategy: fail-fast: false matrix: diff --git a/CMakeLists.txt b/CMakeLists.txt index a57fda40a..fa1f3d039 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py) add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) + add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc7c0ec8c..85fc90d0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ For pull requests, full Linux and Windows builds and tests run only when a colla gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234 ``` -This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main`; full builds never run automatically for pull requests. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. +This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [TechnicalDebt](docs/dev/TechnicalDebt.md), and only UTF-8 text files. Run it locally before pushing: diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 92d5386bb..50fbd61dd 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -18,6 +18,7 @@ RULES = { "extension": ("error", "CONTRIBUTING.md#code", "Avoid non-standard extensions where standard C++ is enough"), "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository"), "binary": ("error", "docs/dev/CONVENTIONS.md", "Only UTF-8 text files in the repository; images go in the gist comments"), + "ci-gate": ("error", "CONTRIBUTING.md#build-and-test", "A job that runs for pull requests needs a file filter: paths on the pull_request trigger, or needs eligibility and an if on its outputs"), "doc-link": ("error", "CONTRIBUTING.md#documentation", "Relative link to a file that does not exist"), "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), @@ -41,6 +42,7 @@ TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") LINK = re.compile(r"\]\(([^)\s]+)") SECTION = re.compile(r"^###\s*(.+?)\s*$", re.M) +KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") def git(*args, text=True): @@ -174,6 +176,20 @@ class Check: if resolved not in self.tree and resolved not in self.dirs: self.report("doc-link", path, number, target) + def workflows(self): + for path in sorted(self.status): + if self.status[path] == "D" or not re.fullmatch(r"\.github/workflows/[^/]+\.ya?ml", path): + continue + source = self.show(path).splitlines() + lines = [(number, KEY.match(re.sub(r"(^|\s)#.*", "", text))) for number, text in enumerate(source, 1)] + lines = [(number, len(match[1]), match[2].strip("'\""), match[3] or "") for number, match in lines if match] + triggers = [entry for entry in lines if entry[1] == 0 and entry[2] == "on"] + if not triggers or pull_request_filtered(lines, triggers[0]): + continue + for number, name, needs, condition in jobs(lines, source): + if name != "eligibility" and ("eligibility" not in needs or "needs.eligibility.outputs." not in condition): + self.report("ci-gate", path, number, name) + def commits(self): for line in git("log", "--no-merges", "--format=%h %s", f"{self.base}..{self.head}").splitlines(): sha, subject = line.split(" ", 1) @@ -232,6 +248,48 @@ def silent_stub_debt(text): return debt +def block(lines, start): + _, indent, _, _ = lines[start] + end = start + 1 + while end < len(lines) and lines[end][1] > indent: + end += 1 + return lines[start + 1:end] + + +def pull_request_filtered(lines, trigger): + _, _, _, value = trigger + if value: + return "pull_request" not in re.findall(r"[A-Za-z_]+", value) + triggers = block(lines, lines.index(trigger)) + for index, (_, indent, name, value) in enumerate(triggers): + if name == "pull_request" and indent == triggers[0][1]: + return any(entry[2] in ("paths", "paths-ignore") for entry in block(triggers, index)) + return True + + +def field(source, entry): + number, indent, _, value = entry + for line in (line for line in source[number:] if line.strip()): + if len(line) - len(line.lstrip(" ")) <= indent: + break + value += " " + line.strip() + return value + + +def jobs(lines, source): + top = [index for index, entry in enumerate(lines) if entry[1] == 0 and entry[2] == "jobs"] + if not top: + return [] + entries = block(lines, top[0]) + result = [] + for index, (number, indent, name, _) in enumerate(entries): + if indent != entries[0][1]: + continue + fields = {entry[2]: field(source, entry) for entry in block(entries, index) if entry[1] == indent + 2} + result.append((number, name, re.findall(r"[A-Za-z0-9_-]+", fields.get("needs", "")), fields.get("if", ""))) + return result + + def listed(debt, name, path): own = PurePosixPath(path).parts[:4] for text, targets in debt: @@ -314,6 +372,7 @@ if __name__ == "__main__": check.build() check.repository() check.docs() + check.workflows() check.commits() pr = json.load(open(args.event, encoding="utf-8")).get("pull_request") if args.event else None if pr: diff --git a/tools/tests/test_check_conventions_ci_gate.py b/tools/tests/test_check_conventions_ci_gate.py new file mode 100644 index 000000000..33f7d9450 --- /dev/null +++ b/tools/tests/test_check_conventions_ci_gate.py @@ -0,0 +1,103 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +CHECKER = Path(__file__).resolve().parents[1] / "check_conventions.py" +WORKFLOWS = Path(__file__).resolve().parents[2] / ".github/workflows" + +GATE = """ eligibility: + runs-on: ubuntu-latest + outputs: + tests: ${{ steps.gate.outputs.tests }} + steps: + - run: echo +""" + + +def workflow(trigger, job): + return f"name: Test\n\non:\n{trigger}\njobs:\n{GATE}{job}" + + +PULL_REQUEST = " pull_request:\n branches: [main]\n" + + +class CiGateTests(unittest.TestCase): + def check(self, files): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + git("init", "-q") + (root / "README.md").write_text("readme\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: add readme") + for name, text in files.items(): + path = root / ".github/workflows" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + git("add", ".") + git("commit", "-qm", "ci: add workflow") + env = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + env.update(GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="safe.directory", GIT_CONFIG_VALUE_0=str(root)) + return subprocess.run( + [sys.executable, "-I", str(CHECKER), "--base", "HEAD~1", "--head", "HEAD"], + cwd=root, capture_output=True, text=True, env=env, timeout=20, + ) + + def assertGated(self, text): + result = self.check({"build.yml": text}) + self.assertNotIn("ci-gate", result.stdout, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def assertUngated(self, text, job): + result = self.check({"build.yml": text}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[ci-gate]", result.stdout) + self.assertIn(job, result.stdout) + + def test_job_without_gate_fails(self): + self.assertUngated(workflow(PULL_REQUEST, " tests:\n runs-on: ubuntu-latest\n steps:\n - run: make\n"), "tests") + + def test_job_needing_the_gate_without_if_fails(self): + self.assertUngated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n runs-on: ubuntu-latest\n"), "tests") + + def test_if_on_another_condition_fails(self): + self.assertUngated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: github.event_name == 'pull_request'\n"), "tests") + + def test_gated_job_passes(self): + self.assertGated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: ${{ needs.eligibility.outputs.tests == 'true' }}\n")) + + def test_gate_written_over_several_lines_passes(self): + self.assertGated(workflow(PULL_REQUEST, " tests:\n needs:\n - other\n - eligibility\n if: >-\n github.event_name == 'push' ||\n needs.eligibility.outputs.tests == 'true'\n")) + + def test_paths_filter_passes(self): + self.assertGated(workflow(" pull_request:\n paths:\n - 'core/**'\n", " tests:\n runs-on: ubuntu-latest\n")) + + def test_paths_of_another_trigger_do_not_count(self): + self.assertUngated(workflow(" push:\n paths:\n - 'core/**'\n pull_request:\n branches: [main]\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + + def test_inline_trigger_list_fails(self): + self.assertUngated("name: Test\non: [push, pull_request]\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") + + def test_other_triggers_pass(self): + self.assertGated(workflow(" pull_request_target:\n types: [opened]\n push:\n branches: [main]\n", " tests:\n runs-on: ubuntu-latest\n")) + + def test_repository_workflows_pass(self): + files = {path.name: path.read_text(encoding="utf-8") for path in WORKFLOWS.glob("*.y*ml")} + self.assertIn("build.yml", files) + result = self.check(files) + self.assertNotIn("ci-gate", result.stdout, result.stdout + result.stderr) + + +if __name__ == "__main__": + unittest.main() From b3587f9528587836cdf3f93d23cfcc5fb03021b7 Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd Date: Mon, 12 Oct 2026 03:52:35 +0400 Subject: [PATCH 309/318] fix(shader): preserve ancillary proofs across relative VGPR accesses --- .../tests/execution/RelativeAncillary.cpp | 148 ++++++++++++++++++ core/shader/recompiler/Recompiler.cpp | 3 + .../src/ControlFlowInstructions.cpp | 34 +++- .../Translation/src/InstructionTranslator.cpp | 10 +- docs/dev/TechnicalDebt.md | 2 +- 5 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp new file mode 100644 index 000000000..98d2d1b89 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp @@ -0,0 +1,148 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; +constexpr std::uint32_t Extent = 64u; +alignas(256) std::array pixels{}; +alignas(256) constexpr std::array, 6> positions{{ + {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; +constexpr std::array vertexCode{ + 0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, std::span fragmentCode, std::uint32_t offset, std::uint32_t waveSize, std::byte expected, bool reject = false, bool implicitInputs = false) { + pixels.fill(std::byte{0x40}); + const auto target = device.Target(); + const auto userData = BufferDescriptor(positions.data(), 16u, positions.size()); + const auto code = std::span(vertexCode); + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(code.data()), code, 0, {}}, + {32u, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, {}}, + target, {0u, 0u, 0u, 64u} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto pushBytes = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + if (implicitInputs) { + pixel.inputAddr |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionZ) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionW); + pixel.posX = pixel.posY = pixel.posZ = pixel.posW = true; + } + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + const std::array pixelData{offset}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(fragmentCode.data()), fragmentCode, 0, {}}, + {waveSize, 0u, pixelData, std::nullopt, pixel, std::nullopt, {}}, target, PixelPushLayout(pushBytes, target) + }; + fragment.useCache = false; + const auto pixelResult = [&] { + try { + auto result = ShaderRecompiler::Recompile(fragment); + Require(!reject, "an unsupported ancillary word was accepted"); + return result; + } catch (const std::exception& error) { + if (!reject || std::string(error.what()).find("packed pixel ancillary input has an unsupported live use") == std::string::npos) throw; + return ShaderRecompiler::RecompileResult{}; + } + }(); + if (reject) return; + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0u}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(pushBytes, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Extent, Extent}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false}; + draw.firstVertex = 3u; + draw.firstInstance = 2u; + device.Draw(state, draw, shaders); + device.WaitIdle(); + for (const auto value : pixels) Require(value == expected, "relative ancillary draw produced an incorrect pixel: " + std::to_string(std::to_integer(value))); +} + +} // namespace + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto waveSize : {32u, 64u}) { + const std::array read{ + 0xbefc0300u, 0x7e028700u, 0xd5480001u, 0x02111101u, + 0x7e020d01u, 0x060202f2u, 0xf800180fu, 0x01010101u + }; + auto code = std::vector(read.begin(), read.end()); + code.push_back(0xbf810000u); + std::cerr << "wave " << waveSize << " packed read\n"; + Draw(*device, code, 0u, waveSize, std::byte{255}); + Draw(*device, code, 4u, waveSize, std::byte{255}, false, true); + const std::array prefix{ + 0xd5480000u, 0x02111100u, 0x7e000d00u, 0x7e020280u, + 0x7e0402f2u, 0xbefc0300u, 0x7e008502u, 0x7e008702u + }; + code.assign(prefix.begin(), prefix.end()); + code.insert(code.end(), {0xf800180fu, 0x01010101u, 0xbf810000u}); + std::cerr << "in-range\n"; + Draw(*device, code, 1u, waveSize, std::byte{255}); + std::cerr << "out-of-range\n"; + Draw(*device, code, 8u, waveSize, std::byte{0}); + code[6] = 0x7e02d100u; + code.erase(code.begin() + 7); + std::cerr << "in-range\n"; + Draw(*device, code, 0x00010001u, waveSize, std::byte{255}); + std::cerr << "out-of-range\n"; + Draw(*device, code, 0x00010008u, waveSize, std::byte{0}); + code.assign(prefix.begin(), prefix.begin() + 6); + code.insert(code.end(), {0xbefe0480u, 0x7e008502u, 0xbefe04c1u, 0xf800180fu, 0x01010101u, 0xbf810000u}); + std::cerr << "inactive write\n"; + Draw(*device, code, 1u, waveSize, std::byte{0}); + for (const auto modifier : {0x0000e402u, 0xff00e402u}) { + code.assign(prefix.begin(), prefix.begin() + 6); + code.insert(code.end(), {0x7e0084fau, modifier, 0xf800180fu, 0x01010101u, 0xbf810000u}); + std::cerr << "DPP write " << modifier << '\n'; + Draw(*device, code, 1u, waveSize, modifier == 0xff00e402u ? std::byte{255} : std::byte{0}); + Draw(*device, code, 8u, waveSize, std::byte{0}); + } + const std::array unsupported{0xbefc0300u, 0x7e028700u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + Draw(*device, unsupported, 0u, waveSize, std::byte{0}, true); + Draw(*device, unsupported, 4u, waveSize, std::byte{0}, true, true); + } + std::cout << "relative ancillary GPU readback tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 2a214225d..23edcd9d4 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -223,6 +223,9 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat } if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { + constantFolder.Fold(program); + ResolveControlFlowIdentities(program); + deadCodeEliminator.RemoveIdentities(program); deadCodeEliminator.Eliminate(program); } }; diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 305327832..b2168d116 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -331,8 +331,12 @@ struct RelativeWindowAccess { IrValue* inRange = nullptr; }; -RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset, const char* name) { +RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset, const char* name, std::uint32_t vectorLimit) { const auto& metadata = program.Metadata(); + if (metadata.vectorWindowSize == 0u) { + if (reg >= vectorLimit) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing span"); + return {nullptr, &ir.ULessThan(offset, ir.Constant(vectorLimit - reg))}; + } if (metadata.vectorWindowSize == 0u || reg < metadata.vectorWindowBase) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing window"); const auto first = reg - metadata.vectorWindowBase; IrValue& index = ir.Emit(IrOpcode::UMin32, IrType::U32, {&ir.IAdd(ir.Constant(first), offset), &ir.Constant(metadata.vectorWindowSize - 1u)}); @@ -343,7 +347,15 @@ IrU32 TranslationContext::readRelativeVector(const RdnaOperand& base, IrValue& o if (base.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " source is not a vector register"); } - const auto access = relativeWindowAccess(ir, program, base.reg, offset, name); + const auto access = relativeWindowAccess(ir, program, base.reg, offset, name, currentVectorLimit); + if (access.index == nullptr) { + IrU32 result(ir.GetVectorReg(static_cast(0))); + for (std::uint32_t reg = base.reg; reg < currentVectorLimit; ++reg) { + IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base.reg)); + result = IrU32(ir.Select(hit, ir.GetVectorReg(static_cast(reg)), result.Value())); + } + return applyBitSourceModifiers(base, result); + } IrValue& loaded = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); return applyBitSourceModifiers(base, IrU32(ir.Select(*access.inRange, loaded, ir.GetVectorReg(static_cast(0))))); } @@ -353,7 +365,21 @@ void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& o if (destination.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " destination is not a vector register"); } - const auto access = relativeWindowAccess(ir, program, destination.reg, offset, name); + const auto access = relativeWindowAccess(ir, program, destination.reg, offset, name, currentVectorLimit); + if (access.index == nullptr) { + for (std::uint32_t reg = destination.reg; reg < currentVectorLimit; ++reg) { + IrValue& previous = ir.GetVectorReg(static_cast(reg)); + IrValue* merged = nullptr; + if (destination.dpp) { + const DppMoveFlags flags{destination.dpp8 ? DppMoveFlags::Lanes8 | destination.dppCtrl : destination.dppCtrl, static_cast(destination.dppRowMask), static_cast(destination.dppBankMask), destination.dppFetchInactive, destination.dppBoundCtrl}; + merged = &ir.Emit(IrOpcode::DppUpdateU32, IrType::U32, {&value.Value(), &previous, &ir.GetExec()}, flags); + } else merged = &ir.Select(ir.GetExec(), value.Value(), previous); + IrValue* written = &ir.IEqual(offset, ir.Constant(reg - destination.reg)); + if (enable != nullptr) written = &ir.LogicalAnd(*written, *enable); + ir.SetVectorReg(static_cast(reg), ir.Select(*written, *merged, previous)); + } + return; + } IrValue& previous = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); IrValue* merged = nullptr; if (destination.dpp) { @@ -387,7 +413,7 @@ void TranslationContext::vMovrelsdB32(const RdnaInstruction& inst, bool split, b return; } const IrU32 previous = readRelativeVector(plainOperand(inst.destination), destinationOffset, name); - IrValue& bothInRange = ir.LogicalAnd(*relativeWindowAccess(ir, program, sourceAt(inst, 0u).reg, sourceOffset, name).inRange, *relativeWindowAccess(ir, program, inst.destination.reg, destinationOffset, name).inRange); + IrValue& bothInRange = ir.LogicalAnd(*relativeWindowAccess(ir, program, sourceAt(inst, 0u).reg, sourceOffset, name, currentVectorLimit).inRange, *relativeWindowAccess(ir, program, inst.destination.reg, destinationOffset, name, currentVectorLimit).inRange); writeRelativeVector(inst.destination, destinationOffset, value, name, &bothInRange); writeRelativeVector(sourceAt(inst, 0u), sourceOffset, previous, name, &bothInRange); } diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 9d54fe93f..04d50e26f 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -435,7 +435,8 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con } } } - if (windowBase < vectorLimit) { + const bool ancillaryWindow = options.stage == ShaderStageKind::Pixel && options.inputInfo.pixel != nullptr && options.inputInfo.pixel->psAncillary && options.inputInfo.pixel->psInputVgpr[static_cast(PixelInput::Ancillary)] >= windowBase; + if (windowBase < vectorLimit && !ancillaryWindow) { program.Metadata().vectorWindowBase = windowBase; program.Metadata().vectorWindowSize = vectorLimit - windowBase; } @@ -501,6 +502,13 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con if (program.Metadata().vectorWindowSize != 0u) { vectorLimit = program.Metadata().vectorWindowBase + program.Metadata().vectorWindowSize; } + if (ancillaryWindow) { + for (const IrValue* instruction : blocks.front()->Instructions()) { + if (instruction->Opcode() == IrOpcode::SetVectorRegister) { + vectorLimit = std::max(vectorLimit, instruction->Argument(0)->Register().index + 1u); + } + } + } for (const auto& cfgBlock : cfg.blocks) { const auto typedIndex = blockIndices.at(cfgBlock.id); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 446bdd0bc..2c126d815 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -663,7 +663,7 @@ Throughout the project, every function at every stage either **does exactly what - [LINE_STIPPLE_TEX](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. -- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. +- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. Whole-word uses still throw. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. - [User-data callees](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. - [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. From ae049e77577ae5b9865e1fa17ff921cba82df1cd Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd Date: Mon, 12 Oct 2026 14:09:32 +0400 Subject: [PATCH 310/318] fix(shader): restrict ancillary refolding to live builtin uses --- core/shader/recompiler/Recompiler.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 23edcd9d4..7b74fc9ce 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -223,9 +223,16 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat } if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { - constantFolder.Fold(program); - ResolveControlFlowIdentities(program); - deadCodeEliminator.RemoveIdentities(program); + const bool liveAncillary = std::ranges::any_of(program.Blocks(), [](const auto& block) { + return std::ranges::any_of(block->Instructions(), [](const IrValue* value) { + return value->Opcode() == IrOpcode::GetBuiltin && value->HasUses() && value->Argument(0)->Resolve()->ImmediateU32() == static_cast(StageInputKind::PackedAncillary); + }); + }); + if (liveAncillary) { + constantFolder.Fold(program); + ResolveControlFlowIdentities(program); + deadCodeEliminator.RemoveIdentities(program); + } deadCodeEliminator.Eliminate(program); } }; From ce0915e83471dd6c0264f7484a7f563221234c36 Mon Sep 17 00:00:00 2001 From: gookie <36005381+gookie-dev@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:12:34 +0200 Subject: [PATCH 311/318] feat(libSceHttp2): implement sceHttp2DeleteCookieBox PPSA35943 (Graveyard Keeper 2) imports sceHttp2DeleteCookieBox, which no library exported. On current main the loader stops the title there: ./eboot.elf: symbol lookup error: ./eboot.elf: undefined symbol: O9ync3F-JVI sceHttp2DeleteCookieBox(cookieBoxId) erases the box from the handle registry sceHttp2CreateCookieBox adds it to and returns 0, or the library's invalid-id error (0x817B1100) for an id that is not a live cookie box. The signature follows SharpProspero and the behaviour prosper's sceHttp2DeleteCookieBox; the error code is listed in TechnicalDebt as not measured on a console. guest_http2 deletes a box twice, a context id, a box whose context was terminated, and the two boxes it uses before sceHttp2Term. With this change the title loads and runs into its asset loading. --- core/libs/prx/libSceHttp2/Export.cpp | 7 +++++++ core/libs/tests/GuestHttp2.cpp | 8 ++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 16 insertions(+) diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index f037bf7f9..786bf3099 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -356,6 +356,13 @@ int APS5_VABI sceHttp2CreateCookieBox(int lib_http2_ctx_id) { return id; } +int APS5_VABI sceHttp2DeleteCookieBox(int cookie_box_id) { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(cookie_box_id, Http2Kind::CookieBox) == nullptr) return ERROR_INVALID_ID; + g_handles.erase(cookie_box_id); + return 0; +} + int APS5_VABI sceHttp2SetCookieBox(int id, int cookie_box_id) { (void)id; (void)cookie_box_id; diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index e64d3053f..ffce322a3 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -8,6 +8,7 @@ int APS5_VABI sceHttp2Init(int, int, std::size_t, int); int APS5_VABI sceHttp2CreateTemplate(int, const char*, int, int); int APS5_VABI sceHttp2CreateRequestWithURL(int, const char*, const char*, std::uint64_t); int APS5_VABI sceHttp2CreateCookieBox(int); +int APS5_VABI sceHttp2DeleteCookieBox(int); int APS5_VABI sceHttp2SetCookieBox(int, int); int APS5_VABI sceHttp2CookieFlush(int); int APS5_VABI sceHttp2SetRequestNoContentLength(int); @@ -71,6 +72,11 @@ int main() { const int box = sceHttp2CreateCookieBox(context); const int other = sceHttp2CreateCookieBox(context); Require(box > 0 && other > 0 && box != other && box != context); + const int discarded = sceHttp2CreateCookieBox(context); + Require(discarded > 0 && discarded != box && discarded != other); + Require(sceHttp2DeleteCookieBox(discarded) == 0); + Require(sceHttp2DeleteCookieBox(discarded) == InvalidId); + Require(sceHttp2DeleteCookieBox(context) == InvalidId); const int tmpl = sceHttp2CreateTemplate(context, "agent", 2, 0); Require(tmpl > 0 && tmpl != box && tmpl != other); Require(sceHttp2SetResolveRetry(tmpl, 3) == 0); @@ -119,6 +125,7 @@ int main() { Require(sceHttp2Term(doomedContext) == 0); RequireSslSetters(doomedContext, InvalidId); RequireSslSetters(doomedContextBox, InvalidId); + Require(sceHttp2DeleteCookieBox(doomedContextBox) == InvalidId); RequireSslSetters(doomedContextTemplate, InvalidId); RequireSslSetters(doomedContextRequest, InvalidId); Require(sceHttp2Term(doomedContext) == InvalidId); @@ -153,6 +160,7 @@ int main() { Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); Require(sceHttp2DeleteRequest(request) == 0); Require(sceKernelDeleteEqueue(eq) == 0); + Require(sceHttp2DeleteCookieBox(box) == 0 && sceHttp2DeleteCookieBox(other) == 0); Require(sceHttp2Term(context) == 0); KernelEqueue templateEqueue = 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 446bdd0bc..1b0998b11 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -68,6 +68,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceQoSSetConnectionAttribute](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) - [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. - [sceHttp2WebSocketSetPingInterval](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. +- [sceHttp2DeleteCookieBox](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA35943's Unity player. The `(int cookieBoxId)` signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs#L215-L217). A box `sceHttp2CreateCookieBox` returned is discarded; an unknown or already deleted id returns the library's invalid-id error (0x817B1100), as [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp#L512-L523) does; not measured on a console - [sceKernelGetProsperoSystemSwVersion](../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified - [sceSystemServiceLaunchApp](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown From 2919dd38cb527cec030a741c51e7307f15c18acd Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Mon, 12 Oct 2026 13:14:02 +0200 Subject: [PATCH 312/318] fix(agc): keep shader modules of pipeline library parts alive for background linking #4152 dropped the device-wide module list that #3173 added, so Pipeline destroys the shader modules right after the library parts are created, also for the parts prebuilt at registration. AMD's Windows driver still reads them during the background link-time-optimized link and faults inside amdvlk64.dll. Hand the modules of each new part to the device's library store again and destroy them in ClearPipelineLibraries. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/PipelineLibrary.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 ++-- .../Graphics/src/PipelineLibrary.cpp | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index d121692b4..1d340829c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -33,7 +33,7 @@ struct OptimizedPipeline { void Release() noexcept; }; -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, bool partsOnly = false); +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, bool partsOnly = false, std::span modules = {}); void WaitForOptimizedPipelines(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 31cd07f34..f173368fd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -478,13 +478,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput return; } if (prebuilding) { - static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, true)); + static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, true, _modules)); for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); prebuilt = true; return; } - if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized); + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules); else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); if (alternateAlpha) { blend.pAttachments = secondChannels.data(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index 7f8ebfca3..db9134959 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -38,6 +38,8 @@ using Key = std::vector; struct DeviceLibraries { PFN_vkDestroyPipeline destroyPipeline = nullptr; PFN_vkDestroyPipelineLayout destroyLayout = nullptr; + PFN_vkDestroyShaderModule destroyModule = nullptr; + std::vector modules; std::map layouts; std::array, 4> parts; PipelineLibraryCounters counters; @@ -100,7 +102,7 @@ std::span PipelineLibraryDynamicStates() { return dynamicStates; } -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, bool partsOnly) { +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, bool partsOnly, std::span modules) { Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states"); auto& store = Libraries(); std::unique_lock lock(store.mutex); @@ -108,6 +110,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip if (device.destroyPipeline == nullptr) { device.destroyPipeline = context.Function("vkDestroyPipeline"); device.destroyLayout = context.Function("vkDestroyPipelineLayout"); + device.destroyModule = context.Function("vkDestroyShaderModule"); } const auto layoutFor = [&](const Key& key, std::optional set) { auto found = device.layouts.find(key); @@ -200,6 +203,15 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip continue; } cache.emplace(key, handle); + if (optimize) { + for (std::uint32_t stage = 0; stage < create.stageCount; ++stage) { + for (auto& module : modules) { + if (module == VK_NULL_HANDLE || module != create.pStages[stage].module) continue; + device.modules.push_back(module); + module = VK_NULL_HANDLE; + } + } + } ++device.counters.built[part]; libraries.push_back(handle); } @@ -264,6 +276,7 @@ void ClearPipelineLibraries(VkDevice device) { for (const auto& [key, handle] : part) libraries.destroyPipeline(device, handle, nullptr); } for (const auto& [key, handle] : libraries.layouts) libraries.destroyLayout(device, handle, nullptr); + for (const auto module : libraries.modules) libraries.destroyModule(device, module, nullptr); store.devices.erase(found); } From cac398cb96eac617699785fd7580eb34aadfb677 Mon Sep 17 00:00:00 2001 From: Del'aRun Date: Mon, 12 Oct 2026 15:39:27 +0400 Subject: [PATCH 313/318] feat(libSceNgs2): implement the Geom reset functions Implement sceNgs2GeomResetListenerParam and sceNgs2GeomResetSourceParam, taken from #1021 and limited to the two reset functions. The Geom structures get their real layouts. sceNgs2GeomCalcListener and sceNgs2GeomApply stay unimplemented. Co-Authored-By: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../prx/libSceNgs2.native/Ngs2Library.cmake | 1 + .../libSceNgs2.native/include/Ngs2Types.hpp | 69 +++++++++++++++++-- core/libs/prx/libSceNgs2.native/src/Geom.cpp | 34 +++++++++ .../libSceNgs2.native/src/Ngs2Internal.hpp | 2 + .../libSceNgs2.native/src/Unimplemented.cpp | 12 ---- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 8 +++ .../prx/libSceNgs2.native/tests/Ngs2Geom.cpp | 57 +++++++++++++++ .../prx/libSceNgs2.native/tests/Ngs2Test.hpp | 2 + docs/dev/TechnicalDebt.md | 1 + 10 files changed, 170 insertions(+), 18 deletions(-) create mode 100644 core/libs/prx/libSceNgs2.native/src/Geom.cpp create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 79d43e8ae..ca10bd524 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake index 06a20f0d4..877276b0b 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -3,6 +3,7 @@ function(add_sce_ngs2_library target) add_library(${target} SHARED EXCLUDE_FROM_ALL ${ngs2Dir}/src/Atrac9.cpp ${ngs2Dir}/src/Custom.cpp + ${ngs2Dir}/src/Geom.cpp ${ngs2Dir}/src/Pan.cpp ${ngs2Dir}/src/Rack.cpp ${ngs2Dir}/src/Render.cpp diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index f7931d83f..fd9cfd40f 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -607,20 +607,79 @@ struct Ngs2PanWork { }; static_assert(sizeof(Ngs2PanWork) == 40); -struct Ngs2GeomListenerParam { - std::uint32_t reserved[32]; +struct Ngs2GeomVector { + float x; + float y; + float z; }; +static_assert(sizeof(Ngs2GeomVector) == 12); + +struct Ngs2GeomCone { + float inner_level; + float inner_angle; + float outer_level; + float outer_angle; +}; +static_assert(sizeof(Ngs2GeomCone) == 16); + +struct Ngs2GeomRolloff { + std::uint32_t model; + float max_distance; + float rolloff_factor; + float reference_distance; +}; +static_assert(sizeof(Ngs2GeomRolloff) == 16); + +struct Ngs2GeomListenerParam { + Ngs2GeomVector position; + Ngs2GeomVector orient_front; + Ngs2GeomVector orient_up; + Ngs2GeomVector velocity; + float sound_speed; + std::uint32_t reserved[2]; +}; +static_assert(sizeof(Ngs2GeomListenerParam) == 60); struct Ngs2GeomListenerWork { - std::uint32_t reserved[64]; + float matrix[4][4]; + Ngs2GeomVector velocity; + float sound_speed; + std::uint32_t coordinate; + std::uint32_t reserved[3]; }; +static_assert(sizeof(Ngs2GeomListenerWork) == 96); struct Ngs2GeomSourceParam { - std::uint32_t reserved[32]; + Ngs2GeomVector position; + Ngs2GeomVector velocity; + Ngs2GeomVector direction; + Ngs2GeomCone cone; + Ngs2GeomRolloff rolloff; + float doppler_factor; + float fbw_level; + float lfe_level; + float max_level; + float min_level; + float radius; + std::uint32_t num_speakers; + std::uint32_t matrix_format; + std::uint32_t reserved[2]; }; +static_assert(sizeof(Ngs2GeomSourceParam) == 108); + +struct Ngs2GeomA3dAttribute { + Ngs2GeomVector position; + float volume; + std::uint32_t reserved[4]; +}; +static_assert(sizeof(Ngs2GeomA3dAttribute) == 32); struct Ngs2GeomAttribute { - std::uint32_t reserved[32]; + float pitch_ratio; + float level[64]; + Ngs2GeomA3dAttribute a3d_attrib; + std::uint32_t reserved[4]; }; +static_assert(sizeof(Ngs2GeomAttribute) == 308); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp new file mode 100644 index 000000000..c5e3624b3 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -0,0 +1,34 @@ +#include + +#include "Ngs2Internal.hpp" + +static constexpr float DEFAULT_SOUND_SPEED = 343.0f; + +int Ngs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { + if (out_listener_param == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + *out_listener_param = {}; + out_listener_param->orient_front.z = 1.0f; + out_listener_param->orient_up.y = 1.0f; + out_listener_param->sound_speed = DEFAULT_SOUND_SPEED; + return SCE_NGS2_OK; +} + +int Ngs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { + if (out_source_param == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + *out_source_param = {}; + out_source_param->direction.z = 1.0f; + out_source_param->cone.inner_level = 1.0f; + out_source_param->cone.inner_angle = 360.0f; + out_source_param->cone.outer_level = 1.0f; + out_source_param->cone.outer_angle = 360.0f; + out_source_param->rolloff.max_distance = 1000000.0f; + out_source_param->rolloff.rolloff_factor = 1.0f; + out_source_param->rolloff.reference_distance = 1.0f; + out_source_param->doppler_factor = 1.0f; + out_source_param->fbw_level = 1.0f; + out_source_param->lfe_level = 1.0f; + out_source_param->max_level = 1.0f; + out_source_param->num_speakers = 2; + out_source_param->matrix_format = 2; + return SCE_NGS2_OK; +} diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 9bed17834..65829bf2f 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -203,5 +203,7 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest); int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers); int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix); +int Ngs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param); +int Ngs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp index 3287f78d9..9cb4cd441 100644 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp @@ -25,18 +25,6 @@ int APS5_VABI sceNgs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2Ge return 0; } -int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { - (void)out_listener_param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { - (void)out_source_param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNgs2ReportRegisterHandler(uint32_t report_type, void* handler, uintptr_t user_data, Ngs2Handle* out_handle) { (void)report_type; (void)handler; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 008c1cd26..bb45dc5b2 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -486,6 +486,14 @@ int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* p return Ngs2PanGetVolumeMatrix(work, params, num_params, matrix_format, out_volume_matrix); } +int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { + return Ngs2GeomResetListenerParam(out_listener_param); +} + +int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { + return Ngs2GeomResetSourceParam(out_source_param); +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp new file mode 100644 index 000000000..ca194eea7 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -0,0 +1,57 @@ +#include "Ngs2Test.hpp" + +#include +#include + +static constexpr float SoundSpeed = 343.0f; + +static void TestLayouts() { + Require(sizeof(Ngs2GeomVector) == 12); + Require(sizeof(Ngs2GeomCone) == 16); + Require(sizeof(Ngs2GeomRolloff) == 16); + Require(sizeof(Ngs2GeomListenerParam) == 60); + Require(sizeof(Ngs2GeomListenerWork) == 96); + Require(sizeof(Ngs2GeomSourceParam) == 108); + Require(sizeof(Ngs2GeomA3dAttribute) == 32); + Require(sizeof(Ngs2GeomAttribute) == 308); +} + +static void TestResetListenerParam() { + Ngs2GeomListenerParam param; + std::memset(¶m, 0x5a, sizeof(param)); + Require(sceNgs2GeomResetListenerParam(¶m) == SCE_NGS2_OK); + Require(param.position.x == 0.0f && param.position.y == 0.0f && param.position.z == 0.0f); + Require(param.velocity.x == 0.0f && param.velocity.y == 0.0f && param.velocity.z == 0.0f); + Require(param.orient_front.x == 0.0f && param.orient_front.y == 0.0f && param.orient_front.z == 1.0f); + Require(param.orient_up.x == 0.0f && param.orient_up.y == 1.0f && param.orient_up.z == 0.0f); + Require(param.sound_speed == SoundSpeed); + Require(param.reserved[0] == 0u && param.reserved[1] == 0u); + Require(sceNgs2GeomResetListenerParam(nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); +} + +static void TestResetSourceParam() { + Ngs2GeomSourceParam param; + std::memset(¶m, 0x5a, sizeof(param)); + Require(sceNgs2GeomResetSourceParam(¶m) == SCE_NGS2_OK); + Require(param.position.x == 0.0f && param.position.y == 0.0f && param.position.z == 0.0f); + Require(param.velocity.x == 0.0f && param.velocity.y == 0.0f && param.velocity.z == 0.0f); + Require(param.direction.x == 0.0f && param.direction.y == 0.0f && param.direction.z == 1.0f); + Require(param.cone.inner_level == 1.0f && param.cone.inner_angle == 360.0f); + Require(param.cone.outer_level == 1.0f && param.cone.outer_angle == 360.0f); + Require(param.rolloff.model == 0u); + Require(param.rolloff.max_distance == 1000000.0f); + Require(param.rolloff.rolloff_factor == 1.0f); + Require(param.rolloff.reference_distance == 1.0f); + Require(param.doppler_factor == 1.0f && param.fbw_level == 1.0f && param.lfe_level == 1.0f); + Require(param.max_level == 1.0f && param.min_level == 0.0f && param.radius == 0.0f); + Require(param.num_speakers == 2u && param.matrix_format == 2u); + Require(param.reserved[0] == 0u && param.reserved[1] == 0u); + Require(sceNgs2GeomResetSourceParam(nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); +} + +int main() { + TestLayouts(); + TestResetListenerParam(); + TestResetSourceParam(); + return 0; +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 51a50d512..6312953af 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -39,6 +39,8 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t, Ngs2VoiceState*, size_t); int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t, uint32_t*); int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t, uint32_t, Ngs2VoicePortInfo*, size_t); int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t, uint32_t, void*, size_t); +int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam*); +int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam*); } inline void Check(bool value, int line) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index edfc5cefd..f0fc6ecc1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -391,6 +391,7 @@ Throughout the project, every function at every stage either **does exactly what - [gethostbyname](../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver - [sceAppContentGetAddcontDownloadProgress](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - imported by PPSA08755, which fails to load without it; added without an implementation because its behavior is unverified. [SharpProspero](https://github.com/SvenGDK/SharpProspero) lists `(uint32_t serviceLabel, entitlementLabel, SceAppContentAddcontDownloadProgress*)`, but no PS5 reference gives the progress layout or the error codes +- [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomCalcListener` and `sceNgs2GeomApply` stay unimplemented - [_sceFiberGetThreadFramePointerAddress](../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured - [sceAgcAcbWaitUntilSafeForRendering](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known From 61180981ce40c712a9fcd6e4abbee6f3e9b31834 Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:02:52 -0300 Subject: [PATCH 314/318] feat(agc): place textures in host memory when device memory runs out --- .../Graphics/include/Context.hpp | 22 +++++++++++++++++++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 5 +---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index d45fc010f..44dc6f43c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -5,7 +5,9 @@ #define VK_NO_PROTOTYPES #endif #include +#include #include +#include #include #include #include @@ -195,6 +197,26 @@ struct Context { } throw std::runtime_error("AGC graphics: required Vulkan memory type is unavailable"); } + + VkResult AllocateDeviceMemory(std::uint32_t mask, VkDeviceSize bytes, VkDeviceMemory* memoryOut, const char* what) const { + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = bytes; + allocation.memoryTypeIndex = MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + const auto allocate = Function("vkAllocateMemory"); + auto result = allocate(device, &allocation, nullptr, memoryOut); + if (result != VK_ERROR_OUT_OF_DEVICE_MEMORY) return result; + for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { + if ((mask & (1u << i)) == 0 || (memory.memoryHeaps[memory.memoryTypes[i].heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0) continue; + allocation.memoryTypeIndex = i; + result = allocate(device, &allocation, nullptr, memoryOut); + static std::atomic fallbacks{0}, fallbackBytes{0}; + const auto count = fallbacks.fetch_add(1) + 1; + const auto total = fallbackBytes.fetch_add(bytes) + bytes; + if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] %s: device memory exhausted, %llu bytes placed in host memory type %u (%s); %llu such allocations, %llu MiB\n", what, static_cast(bytes), i, result == VK_SUCCESS ? "ok" : "failed", static_cast(count), static_cast(total >> 20u)); + return result; + } + return result; + } }; inline void EndRenderPass(const Context& context, VkCommandBuffer commands) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2753e91fc..6b94a41da 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -283,11 +283,8 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex VkMemoryRequirements requirements{}; context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); - VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; - allocation.allocationSize = requirements.size; allocationBytes = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory texture"); + Check(context.AllocateDeviceMemory(requirements.memoryTypeBits, requirements.size, &owned->memory, "texture"), "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); { From eb03e0e15463dffab6d3dec8e7589aad9d179879 Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:02:52 -0300 Subject: [PATCH 315/318] feat(agc): place storage images in host memory when device memory runs out --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 6b94a41da..bd8e8eec4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -934,11 +934,8 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage storage"); VkMemoryRequirements requirements{}; context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); - VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; - allocation.allocationSize = requirements.size; memoryBytes = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture"); + Check(context.AllocateDeviceMemory(requirements.memoryTypeBits, requirements.size, &memory, "storage texture"), "vkAllocateMemory storage texture"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); From 85929389b59684e8814579217781500fbb16bff1 Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:58:24 -0300 Subject: [PATCH 316/318] test(agc): cover the host memory fallback of device allocations --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 +++ .../tests/DeviceMemoryFallback.cpp | 91 +++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d7f9bef59..1aa11718d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -595,6 +595,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_device_memory_fallback_tests EXCLUDE_FROM_ALL tests/DeviceMemoryFallback.cpp) +target_include_directories(agc_device_memory_fallback_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_device_memory_fallback_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_device_memory_fallback_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_device_memory_fallback_tests) +if(BUILD_TESTING) + set_target_properties(agc_device_memory_fallback_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_device_memory_fallback COMMAND agc_device_memory_fallback_tests) + if(WIN32) + set_tests_properties(agc_device_memory_fallback PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_display_buffer_dcc_tests EXCLUDE_FROM_ALL tests/DisplayBufferDcc.cpp) target_include_directories(agc_display_buffer_dcc_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_display_buffer_dcc_tests PRIVATE libSceAgcDriver libc) diff --git a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp new file mode 100644 index 000000000..43cdbae69 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp @@ -0,0 +1,91 @@ +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace AgcDriver::Graphics; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +constexpr std::uint32_t HostType = 0; +constexpr std::uint32_t DeviceType = 1; + +struct MockDevice { + std::deque results; + std::vector types; + std::uint64_t next = 1; +}; + +MockDevice mock; + +VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { + mock.types.push_back(info->memoryTypeIndex); + auto result = VK_SUCCESS; + if (!mock.results.empty()) { + result = mock.results.front(); + mock.results.pop_front(); + } + *memory = result == VK_SUCCESS ? reinterpret_cast(static_cast(mock.next++)) : VK_NULL_HANDLE; + return result; +} + +PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { + if (std::string_view(name) == "vkAllocateMemory") return reinterpret_cast(mockAllocateMemory); + return nullptr; +} + +Context mockContext() { + Context context{}; + context.deviceProc = mockProc; + context.memory.memoryHeapCount = 2; + context.memory.memoryHeaps[0] = {4096ull << 20u, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + context.memory.memoryHeaps[1] = {16384ull << 20u, 0}; + context.memory.memoryTypeCount = 2; + context.memory.memoryTypes[HostType] = {0, 1}; + context.memory.memoryTypes[DeviceType] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0}; + return context; +} + +void Allocate(const char* what, std::deque results, std::uint32_t mask, VkResult expected, std::vector types) { + mock = MockDevice{}; + mock.results = std::move(results); + const auto context = mockContext(); + VkDeviceMemory memory = VK_NULL_HANDLE; + const auto result = context.AllocateDeviceMemory(mask, 1u << 20u, &memory, "test"); + Expect(result == expected, std::string(what) + ": unexpected result " + std::to_string(result)); + Expect(mock.types == types, std::string(what) + ": unexpected memory types tried"); + Expect((result == VK_SUCCESS) == (memory != VK_NULL_HANDLE), std::string(what) + ": memory handle does not match the result"); +} + +} + +int main() { + try { + const std::uint32_t both = (1u << HostType) | (1u << DeviceType); + Allocate("device memory available", {}, both, VK_SUCCESS, {DeviceType}); + Allocate("device memory exhausted", {VK_ERROR_OUT_OF_DEVICE_MEMORY}, both, VK_SUCCESS, {DeviceType, HostType}); + Allocate("no host type allowed", {VK_ERROR_OUT_OF_DEVICE_MEMORY}, 1u << DeviceType, VK_ERROR_OUT_OF_DEVICE_MEMORY, {DeviceType}); + Allocate("host memory exhausted", {VK_ERROR_OUT_OF_HOST_MEMORY}, both, VK_ERROR_OUT_OF_HOST_MEMORY, {DeviceType}); + Allocate("device lost", {VK_ERROR_DEVICE_LOST}, both, VK_ERROR_DEVICE_LOST, {DeviceType}); + Allocate("fallback fails", {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}, both, VK_ERROR_OUT_OF_HOST_MEMORY, {DeviceType, HostType}); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL: %s\n", error.what()); + return 1; + } + if (failures != 0) return 1; + std::puts("device memory fallback tests passed"); + return 0; +} From b35117c2857d6b6e983911ca3857623fe02654a5 Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:07:23 -0300 Subject: [PATCH 317/318] fix(agc): choose host memory from the heap budget instead of retrying Read the device-local heap's VK_EXT_memory_budget figures before a texture or storage image allocation and pick a host memory type when the allocation does not fit, rather than allocating again after the driver refused the first attempt. The allocation itself is made once and still throws when it fails. --- .../Graphics/include/Context.hpp | 32 +++++---- .../libSceAgcDriver/Graphics/src/Texture.cpp | 10 ++- .../tests/DeviceMemoryFallback.cpp | 70 +++++++------------ 3 files changed, 50 insertions(+), 62 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 44dc6f43c..6ca5f6c43 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -198,24 +198,26 @@ struct Context { throw std::runtime_error("AGC graphics: required Vulkan memory type is unavailable"); } - VkResult AllocateDeviceMemory(std::uint32_t mask, VkDeviceSize bytes, VkDeviceMemory* memoryOut, const char* what) const { - VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; - allocation.allocationSize = bytes; - allocation.memoryTypeIndex = MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - const auto allocate = Function("vkAllocateMemory"); - auto result = allocate(device, &allocation, nullptr, memoryOut); - if (result != VK_ERROR_OUT_OF_DEVICE_MEMORY) return result; + std::uint32_t DeviceMemoryType(std::uint32_t mask, VkDeviceSize bytes, const char* what) const { + const auto type = MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return type; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + memoryProperties2(physical, &properties); + const auto heap = memory.memoryTypes[type].heapIndex; + const auto budget = reported.heapBudget[heap]; + const auto usage = reported.heapUsage[heap]; + if (budget == 0 || (usage < budget && bytes <= budget - usage)) return type; for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { if ((mask & (1u << i)) == 0 || (memory.memoryHeaps[memory.memoryTypes[i].heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0) continue; - allocation.memoryTypeIndex = i; - result = allocate(device, &allocation, nullptr, memoryOut); - static std::atomic fallbacks{0}, fallbackBytes{0}; - const auto count = fallbacks.fetch_add(1) + 1; - const auto total = fallbackBytes.fetch_add(bytes) + bytes; - if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] %s: device memory exhausted, %llu bytes placed in host memory type %u (%s); %llu such allocations, %llu MiB\n", what, static_cast(bytes), i, result == VK_SUCCESS ? "ok" : "failed", static_cast(count), static_cast(total >> 20u)); - return result; + static std::atomic placements{0}, placedBytes{0}; + const auto count = placements.fetch_add(1) + 1; + const auto total = placedBytes.fetch_add(bytes) + bytes; + if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] %s: device-local heap %u uses %llu of its %llu MiB budget, %llu bytes placed in host memory type %u; %llu such allocations, %llu MiB\n", what, heap, static_cast(usage >> 20u), static_cast(budget >> 20u), static_cast(bytes), i, static_cast(count), static_cast(total >> 20u)); + return i; } - return result; + return type; } }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index bd8e8eec4..a19083907 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -283,8 +283,11 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex VkMemoryRequirements requirements{}; context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; allocationBytes = requirements.size; - Check(context.AllocateDeviceMemory(requirements.memoryTypeBits, requirements.size, &owned->memory, "texture"), "vkAllocateMemory texture"); + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); { @@ -934,8 +937,11 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage storage"); VkMemoryRequirements requirements{}; context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; memoryBytes = requirements.size; - Check(context.AllocateDeviceMemory(requirements.memoryTypeBits, requirements.size, &memory, "storage texture"), "vkAllocateMemory storage texture"); + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); diff --git a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp index 43cdbae69..c139312bf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp @@ -1,12 +1,8 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include #include -#include #include -#include #include -#include -#include using namespace AgcDriver::Graphics; @@ -22,52 +18,35 @@ void Expect(bool condition, const std::string& what) { constexpr std::uint32_t HostType = 0; constexpr std::uint32_t DeviceType = 1; +constexpr std::uint64_t MiB = 1ull << 20u; -struct MockDevice { - std::deque results; - std::vector types; - std::uint64_t next = 1; -}; +std::uint64_t reportedBudget = 0; +std::uint64_t reportedUsage = 0; -MockDevice mock; - -VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { - mock.types.push_back(info->memoryTypeIndex); - auto result = VK_SUCCESS; - if (!mock.results.empty()) { - result = mock.results.front(); - mock.results.pop_front(); - } - *memory = result == VK_SUCCESS ? reinterpret_cast(static_cast(mock.next++)) : VK_NULL_HANDLE; - return result; +VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + reported->heapBudget[0] = reportedBudget; + reported->heapUsage[0] = reportedUsage; } -PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { - if (std::string_view(name) == "vkAllocateMemory") return reinterpret_cast(mockAllocateMemory); - return nullptr; -} - -Context mockContext() { +Context mockContext(bool budgetExtension) { Context context{}; - context.deviceProc = mockProc; + context.physical = reinterpret_cast(static_cast(1)); + context.memoryProperties2 = budgetExtension ? mockMemoryProperties2 : nullptr; context.memory.memoryHeapCount = 2; - context.memory.memoryHeaps[0] = {4096ull << 20u, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; - context.memory.memoryHeaps[1] = {16384ull << 20u, 0}; + context.memory.memoryHeaps[0] = {4096 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + context.memory.memoryHeaps[1] = {16384 * MiB, 0}; context.memory.memoryTypeCount = 2; context.memory.memoryTypes[HostType] = {0, 1}; context.memory.memoryTypes[DeviceType] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0}; return context; } -void Allocate(const char* what, std::deque results, std::uint32_t mask, VkResult expected, std::vector types) { - mock = MockDevice{}; - mock.results = std::move(results); - const auto context = mockContext(); - VkDeviceMemory memory = VK_NULL_HANDLE; - const auto result = context.AllocateDeviceMemory(mask, 1u << 20u, &memory, "test"); - Expect(result == expected, std::string(what) + ": unexpected result " + std::to_string(result)); - Expect(mock.types == types, std::string(what) + ": unexpected memory types tried"); - Expect((result == VK_SUCCESS) == (memory != VK_NULL_HANDLE), std::string(what) + ": memory handle does not match the result"); +void Choose(const char* what, bool budgetExtension, std::uint64_t budget, std::uint64_t usage, std::uint32_t mask, std::uint32_t expected) { + reportedBudget = budget; + reportedUsage = usage; + const auto type = mockContext(budgetExtension).DeviceMemoryType(mask, 64 * MiB, "test"); + Expect(type == expected, std::string(what) + ": chose memory type " + std::to_string(type) + ", expected " + std::to_string(expected)); } } @@ -75,17 +54,18 @@ void Allocate(const char* what, std::deque results, std::uint32_t mask int main() { try { const std::uint32_t both = (1u << HostType) | (1u << DeviceType); - Allocate("device memory available", {}, both, VK_SUCCESS, {DeviceType}); - Allocate("device memory exhausted", {VK_ERROR_OUT_OF_DEVICE_MEMORY}, both, VK_SUCCESS, {DeviceType, HostType}); - Allocate("no host type allowed", {VK_ERROR_OUT_OF_DEVICE_MEMORY}, 1u << DeviceType, VK_ERROR_OUT_OF_DEVICE_MEMORY, {DeviceType}); - Allocate("host memory exhausted", {VK_ERROR_OUT_OF_HOST_MEMORY}, both, VK_ERROR_OUT_OF_HOST_MEMORY, {DeviceType}); - Allocate("device lost", {VK_ERROR_DEVICE_LOST}, both, VK_ERROR_DEVICE_LOST, {DeviceType}); - Allocate("fallback fails", {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}, both, VK_ERROR_OUT_OF_HOST_MEMORY, {DeviceType, HostType}); + Choose("no VK_EXT_memory_budget", false, 0, 0, both, DeviceType); + Choose("budget not reported", true, 0, 4000 * MiB, both, DeviceType); + Choose("room in the device-local budget", true, 3000 * MiB, 2000 * MiB, both, DeviceType); + Choose("allocation fills the budget exactly", true, 3000 * MiB, 2936 * MiB, both, DeviceType); + Choose("allocation exceeds the budget", true, 3000 * MiB, 2937 * MiB, both, HostType); + Choose("usage already over the budget", true, 2952 * MiB, 3633 * MiB, both, HostType); + Choose("no host type allowed", true, 3000 * MiB, 3000 * MiB, 1u << DeviceType, DeviceType); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; } if (failures != 0) return 1; - std::puts("device memory fallback tests passed"); + std::puts("device memory placement tests passed"); return 0; } From ca99658cef483ef68b808652782f2772dad0493c Mon Sep 17 00:00:00 2001 From: ThatsLeo Date: Sat, 10 Oct 2026 18:12:13 +0200 Subject: [PATCH 318/318] perf(agc): memoize untracked texture comparisons by the stores that reach the surface The memo stamp held TrackerGeneration, which every collect and driver store advances, so it almost never matched. Driver stores into uncovered ranges record their generation per 64 KiB block, and NewestStamp of the surface replaces TrackerGeneration in the stamp. --- .../Execution/include/GuestMemory.hpp | 1 + .../Execution/src/GuestMemory.cpp | 52 ++++++++++++++++++- .../libSceAgcDriver/Graphics/src/Texture.cpp | 2 +- .../libSceAgcDriver/tests/WriteTracking.cpp | 27 ++++++++++ docs/dev/ARCHITECTURE.md | 11 ++++ 5 files changed, 90 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 5321424ce..b118da7bf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -101,6 +101,7 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes); // tracker mutex, so every collect that bumped before it has finished its walk and every later // collect stamps newer. std::uint64_t TrackerGeneration(); +std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes); // Whether no CPU write touched the range since `generation`: an uncached resetting collect of the // range's pages first (a game store not collected yet becomes a block stamp), then the compare over // the stamps collects make (dirty pages), not the MarkWritten stamps of the driver's own GPU label diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index de2fb32eb..f14a24087 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -859,6 +859,40 @@ struct WriteTracker { #endif } + static constexpr std::size_t UnstampedSlots = 1u << 16; + std::vector unstampedStores; + + void noteUnstampedStore(std::uint64_t address, std::uint64_t end) { + if (unstampedStores.empty()) unstampedStores.assign(UnstampedSlots, 0); + const auto first = address / WriteBlockBytes; + const auto last = (end - 1) / WriteBlockBytes; + if (last - first >= UnstampedSlots) { + std::fill(unstampedStores.begin(), unstampedStores.end(), generation); + return; + } + for (auto block = first; block <= last; ++block) unstampedStores[block % UnstampedSlots] = generation; + } + + std::uint32_t newestStamp(std::uint64_t address, std::uint64_t end) const { + std::uint32_t newest = 0; + if (!unstampedStores.empty()) { + const auto first = address / WriteBlockBytes; + const auto last = (end - 1) / WriteBlockBytes; + if (last - first >= UnstampedSlots) newest = *std::max_element(unstampedStores.begin(), unstampedStores.end()); + else for (auto block = first; block <= last; ++block) newest = std::max(newest, unstampedStores[block % UnstampedSlots]); + } + if (!watched) return newest; +#ifdef _WIN32 + address = std::max(address, base); + end = std::min(end, base + size); +#else + end = std::min(end, LeafCount * LeafBlocks * WriteBlockBytes); +#endif + if (address >= end) return newest; + for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) newest = std::max(newest, stampOf(block)); + return newest; + } + bool unchanged(std::uint64_t first, std::uint64_t last, std::uint64_t since) const { if (first == last) return stampOf(first) <= since; const auto newer = [since](std::uint32_t stamp) { return stamp > since; }; @@ -1235,7 +1269,10 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func const auto stampStored = [](WriteTracker& tracker, std::pair stored) -> std::uint64_t { if (stored.second <= stored.first) return 0; ++tracker.generation; - if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) return 0; + if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) { + tracker.noteUnstampedStore(stored.first, stored.second); + return 0; + } for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation); @@ -1280,7 +1317,10 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { tracker.initialize(); if (bytes == 0) return 0; ++tracker.generation; - if (!tracker.watched || !tracker.covers(address, bytes)) return 0; + if (!tracker.watched || !tracker.covers(address, bytes)) { + tracker.noteUnstampedStore(address, address + bytes); + return 0; + } #ifndef _WIN32 { const auto blockFirst = address & ~(std::uint64_t{WriteBlockBytes} - 1); @@ -1288,6 +1328,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { StampRuns runs{tracker, StampKind::Fresh}; if (!GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs)) { unwatchLocked(tracker, blockFirst, static_cast(blockStop - blockFirst)); + tracker.noteUnstampedStore(address, address + bytes); return 0; } if (runs.stamped != 0) ++tracker.generation; @@ -1347,6 +1388,13 @@ std::uint64_t TrackerGeneration() { return tracker.generation; } +std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + return bytes == 0 ? 0 : tracker.newestStamp(address, address + bytes); +} + bool UnchangedSinceCollected(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { if (CollectWritesUncached(address, bytes) == 0) return false; auto& tracker = Tracker(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2753e91fc..65de3e7d3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1909,7 +1909,7 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, const auto first = address / blockBytes; Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs"); const auto* recorder = Recorder::Active(); - const std::array stamp{GuestMemory::CollectEpoch(), GuestMemory::TrackerGeneration(), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + const std::array stamp{GuestMemory::CollectEpoch(), GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; const bool cacheable = memoize && stamp[0] != 0 && (stamp[2] & 1u) == 0 && address == descriptor.baseAddress && bytes == guestBytes; if (cacheable && comparedGuestBytes == stamp) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index b924f0d5c..f3b9b844b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -152,6 +152,32 @@ void CheckUnwatch() { Require(!UnchangedSince(base, Block, adjacent), "unwatch hid a CPU edit in unrelated memory"); } +void CheckNewestStamp() { + void* memory = AllocateWatched(4 * Block); + const auto base = reinterpret_cast(memory); + const auto first = base + Block; + const auto second = base + 2 * Block; + std::memset(memory, 0x11, 4 * Block); + BumpCollectEpoch(); + CollectWrites(base, 4 * Block); + Unwatch(first, 2 * Block); + const auto firstBefore = NewestStamp(first, Block); + const auto secondBefore = NewestStamp(second, Block); + MarkWritten(second + 16, 4); + Require(NewestStamp(second, Block) > secondBefore, "a driver store into unwatched memory did not advance its stamp"); + Require(NewestStamp(first, Block) == firstBefore, "a driver store advanced the stamp of other unwatched memory"); + const auto generation = TrackerGeneration(); + static_cast(memory)[0] = 0x22; + CollectWritesUncached(base, Block); + Require(TrackerGeneration() > generation && NewestStamp(first, Block) == firstBefore, "a CPU store into watched memory advanced the stamp of unwatched memory"); + StoreOwnBytes(first, 1, [&] { *reinterpret_cast(first) = 0x42; }); + Require(NewestStamp(first, Block) > firstBefore, "an own store into unwatched memory did not advance its stamp"); + const auto partial = NewestStamp(base + 3 * Block - 8, Block + 8); + static_cast(memory)[3 * Block] = 0x33; + CollectWritesUncached(base + 3 * Block, Block); + Require(NewestStamp(base + 3 * Block - 8, Block + 8) > partial, "a CPU store into the watched part of a partly watched range did not advance its stamp"); +} + #ifdef _WIN32 void CheckPrivateMappingReuse() { void* memory = AllocateWatched(3 * Block); @@ -310,6 +336,7 @@ int main() { CheckSharedBlock(); CheckOwnStore(); CheckUnwatch(); + CheckNewestStamp(); #ifdef _WIN32 CheckPrivateMappingReuse(); #endif diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 4d5121846..a1d0da4e4 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -97,6 +97,17 @@ flowchart LR - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. +### Untracked storage image comparisons + +A storage image over guest memory that the write watch does not cover (a host-pointer import on a driver that drops its write protection, or an unwatched range) has no block stamps to tell a CPU edit apart, so `StorageTexture::compareUntracked` in [`Texture.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) compares the guest bytes with the image's snapshot. A whole-surface comparison that found no change is remembered under a stamp of: + +- the calling queue worker's collect epoch (`GuestMemory::CollectEpoch`), which advances at every ordering point where a CPU store may have landed (submission start, a satisfied `WAIT_REG_MEM`, a drain); a thread that never bumps it gets epoch 0 and is never memoized; +- `GuestMemory::NewestStamp` of the surface: the newest block stamp of its watched part, and the generation of the newest driver store (`MarkWritten`, `StoreOwnBytes`) into its unwatched part; +- `GuestMemory::ForgetSerial`, which changes when guest memory is released or remapped; +- the recorder's newest pending GPU write note over the surface. + +The comparison runs again when any of them changes. A CPU store into unwatched memory is therefore seen at the next ordering point, the same bound the collect memo gives watched memory. Driver stores into unwatched memory are recorded per 64 KiB block in a fixed table of 65,536 slots indexed by block number modulo the table size, each slot holding the newest generation stored to any of its blocks. A collision can only raise a surface's stamp, which costs one comparison and never skips one. `agc_driver_dcc_keys_first_write` (`RunUntrackedReuse`) checks that a CPU edit to an unwatched image is uploaded after the next epoch, and `agc_write_tracking` (`CheckNewestStamp`) checks which stores advance `NewestStamp`. + ### Shader MODE register access `GuestContext::floatMode` carries the initial shader mode through `Recompile` and `TranslateOptions` into each block's `TranslationContext`. `s_getreg_b32` reads confined to MODE bits 0–3 return the selected initial rounding bits in the destination's low bits. Bits 0–1 select f32 rounding; bits 2–3 select f16/f64 rounding. Missing mode metadata retains the legacy round-to-nearest-even assumption. Reading other MODE fields or hardware registers throws.