From cd16c0a993a21d14eaf77675498f404e08aba777 Mon Sep 17 00:00:00 2001 From: Utku Halis Date: Sun, 11 Oct 2026 07:32:47 +0300 Subject: [PATCH 001/702] fix(shader): take the fused result when an f16 fma's sum is exact fmaF16RoundedToOdd adds the f32 product and the addend separately and uses TwoSum to round to odd when the sum is inexact. When the error is zero it returned the separate sum. On MoltenVK (Apple M3 Pro) the separate multiply and add produce +0 where the IEEE result is -0: agc_driver_dpp16_operands got 0x0000 instead of 0x8000 in the high half of lane 1's row_shr v_pk_fmac_f16. The exact case now returns an f32 fma of the same operands. The product of two f16 values is exact in f32 (11-bit significands, exponents well inside the f32 range), so when TwoSum's error is zero the separate sum and the fused result are the same IEEE value, including the sign of zero; the change only matters on devices whose separate multiply and add do not keep it. The inexact path is unchanged. --- core/shader/recompiler/Translation/src/FloatInstructions.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 2ca7925c1f..0aef745417 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -262,7 +262,8 @@ IrF32 TranslationContext::fmaF16RoundedToOdd(IrF32 lhs, IrF32 rhs, IrF32 addend) IrValue& sumBits = ir.BitCastU32(sum); IrValue& sameSign = ir.IEqual(ir.BitwiseAnd(ir.BitwiseXor(sumBits, ir.BitCastU32(error)), ir.Constant(0x80000000u)), ir.Constant(0u)); IrValue& towardZero = ir.Select(sameSign, sumBits, ir.ISub(sumBits, ir.Constant(1u))); - return IrF32(ir.BitCastF32(ir.Select(inexact, ir.BitwiseOr(towardZero, ir.Constant(1u)), sumBits))); + IrValue& fused = ir.BitCastU32(ir.Emit(IrOpcode::FPFma32, IrType::F32, {&lhs.Value(), &rhs.Value(), &addend.Value()})); + return IrF32(ir.BitCastF32(ir.Select(inexact, ir.BitwiseOr(towardZero, ir.Constant(1u)), fused))); } IrU32 TranslationContext::readF16Bits(const RdnaOperand& operand) { From 4e8bd1609ceedd0ee12094c1238db6804ca766e9 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Mon, 19 Oct 2026 09:49:15 +0300 Subject: [PATCH 002/702] fix(savedata): build libSceSaveData and libSceSaveDataDialog from the native sources PS4 titles import save data from libSceSaveData and libSceSaveDataDialog, and a converted title loads exactly the modules its DT_NEEDED names. Those two libraries were separate, older implementations that export only 9 and 8 functions, while libSceSaveData.native and libSceSaveDataDialog.native carry sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataSetParam, sceSaveDataSaveIcon, sceSaveDataDialogUpdateStatus and the rest. A PS4 title importing any of those failed at startup with unresolved imports. Both plain libraries are now built from the native sources, the way libSceNgs2 is built from libSceNgs2.native, and the old implementation and its test variant are removed. The earlier implementation's save-data memory and mount alias files are not read; that is recorded in the debt file. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 10 - .../prx/libSceSaveData.native/CMakeLists.txt | 5 +- .../SaveDataLibrary.cmake | 5 + core/libs/prx/libSceSaveData/CMakeLists.txt | 2 + core/libs/prx/libSceSaveData/Export.cpp | 94 -- core/libs/prx/libSceSaveData/SaveDataCore.hpp | 1326 ----------------- .../CMakeLists.txt | 5 +- .../SaveDataDialogLibrary.cmake | 5 + .../prx/libSceSaveDataDialog/CMakeLists.txt | 2 + core/libs/prx/libSceSaveDataDialog/Export.cpp | 67 - core/libs/tests/SaveDataWriteReplacement.cpp | 23 +- docs/dev/debt/libSceSaveData.md | 5 + 12 files changed, 24 insertions(+), 1525 deletions(-) create mode 100644 core/libs/prx/libSceSaveData.native/SaveDataLibrary.cmake create mode 100644 core/libs/prx/libSceSaveData/CMakeLists.txt delete mode 100644 core/libs/prx/libSceSaveData/Export.cpp delete mode 100644 core/libs/prx/libSceSaveData/SaveDataCore.hpp create mode 100644 core/libs/prx/libSceSaveDataDialog.native/SaveDataDialogLibrary.cmake create mode 100644 core/libs/prx/libSceSaveDataDialog/CMakeLists.txt delete mode 100644 core/libs/prx/libSceSaveDataDialog/Export.cpp create mode 100644 docs/dev/debt/libSceSaveData.md diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index fa973fa7e4..331cfae61d 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -527,15 +527,8 @@ if(BUILD_TESTING) 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}) - target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData) - configure_windows_unwind(savedata_core_write_replacement_tests) - add_test(NAME savedata_core_write_replacement COMMAND savedata_core_write_replacement_tests) - add_test_executable(savedata_native_write_replacement_tests tests/SaveDataWriteReplacement.cpp) target_include_directories(savedata_native_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_compile_definitions(savedata_native_write_replacement_tests PRIVATE SAVEDATA_NATIVE_BACKEND) target_link_libraries(savedata_native_write_replacement_tests PRIVATE libSceSaveData.native) configure_windows_unwind(savedata_native_write_replacement_tests) add_test(NAME savedata_native_write_replacement COMMAND savedata_native_write_replacement_tests) @@ -556,13 +549,10 @@ if(BUILD_TESTING) endif() if(MINGW) - target_compile_options(savedata_core_write_replacement_tests PRIVATE -fasynchronous-unwind-tables) target_compile_options(savedata_native_write_replacement_tests PRIVATE -fasynchronous-unwind-tables) target_compile_options(savedata_replace_file_failure_tests PRIVATE -fasynchronous-unwind-tables) endif() if(WIN32) - set_tests_properties(savedata_core_write_replacement PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") set_tests_properties(savedata_native_write_replacement PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 6b03a2c86c..fdd264007e 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -1,6 +1,5 @@ -add_library(libSceSaveData.native SHARED EXCLUDE_FROM_ALL Export.cpp) -target_include_directories(libSceSaveData.native PRIVATE ${LIBS_INCLUDE_DIR}) -configure_windows_unwind(libSceSaveData.native) +include(${CMAKE_CURRENT_SOURCE_DIR}/SaveDataLibrary.cmake) +add_sce_savedata_library(libSceSaveData.native) if(BUILD_TESTING) add_test_executable(guest_savedata_search_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataSearch.cpp) diff --git a/core/libs/prx/libSceSaveData.native/SaveDataLibrary.cmake b/core/libs/prx/libSceSaveData.native/SaveDataLibrary.cmake new file mode 100644 index 0000000000..9a7252c375 --- /dev/null +++ b/core/libs/prx/libSceSaveData.native/SaveDataLibrary.cmake @@ -0,0 +1,5 @@ +function(add_sce_savedata_library target) + add_library(${target} SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp) + target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceSaveData/CMakeLists.txt b/core/libs/prx/libSceSaveData/CMakeLists.txt new file mode 100644 index 0000000000..ed878f0e32 --- /dev/null +++ b/core/libs/prx/libSceSaveData/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceSaveData.native/SaveDataLibrary.cmake) +add_sce_savedata_library(libSceSaveData) diff --git a/core/libs/prx/libSceSaveData/Export.cpp b/core/libs/prx/libSceSaveData/Export.cpp deleted file mode 100644 index 0d44da8d1b..0000000000 --- a/core/libs/prx/libSceSaveData/Export.cpp +++ /dev/null @@ -1,94 +0,0 @@ -#include -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libSceSaveData/SaveDataCore.hpp" -#include "prx/libc/include/General.hpp" - -// Savedata imports bind by NID either to libSceSaveData.native or (via the relinker alias table, -// by name) to this library. Every entry point used to return SoftFailNp (0x800242e2), which is the -// NP-family "online service not available" code - wrong dialect for a purely local save call and -// fatal-looking for callers. All entry points now delegate to SaveDataCore.hpp: a local, -// file-backed implementation that needs no PSN account and tolerates any userId -// (0 / 65535 / 268435456), mirroring the native library. - -namespace { -// Logs the first three calls of each entry point, then stays silent, keeping guest logs readable. -#define SAVEDATA_TRACE(...) \ - do { \ - static std::atomic traceCount{0}; \ - if (traceCount.fetch_add(1, std::memory_order_relaxed) < 3) { \ - std::fprintf(stderr, "[SAVEDATA:core] " __VA_ARGS__); \ - std::fputc('\n', stderr); \ - std::fflush(stderr); \ - } \ - } while (0) -} // namespace - -extern "C" { - -int APS5_VABI sceSaveDataInitialize3(const void* init) { - const int rc = savedata::initialize_internal(init); - SAVEDATA_TRACE("sceSaveDataInitialize3 -> 0x%08x", static_cast(rc)); - return rc; -} -int APS5_VABI sceSaveDataTerminate(void) { - const int rc = savedata::terminate_internal(); - SAVEDATA_TRACE("sceSaveDataTerminate -> 0x%08x", static_cast(rc)); - return rc; -} -int APS5_VABI sceSaveDataBackup(const SaveDataBackup* backup) { - const int rc = savedata::backup_internal(backup, nullptr); - SAVEDATA_TRACE("sceSaveDataBackup -> 0x%08x", static_cast(rc)); - return rc; -} -int APS5_VABI sceSaveDataGetEventResult(const void* event_param, SaveDataEvent* event) { - const int rc = savedata::get_event_result_internal(event_param, event); - SAVEDATA_TRACE("sceSaveDataGetEventResult -> 0x%08x", static_cast(rc)); - return rc; -} -int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) { - if (get_param == nullptr) { - return savedata::SD_ERROR_PARAMETER; - } - const int rc = savedata::get_memory_internal(get_param); - SAVEDATA_TRACE("get2 user=%d slot=%u -> 0x%08x", static_cast(get_param->user_id), - static_cast(get_param->slot_id), static_cast(rc)); - return rc; -} -int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param) { - if (set_param == nullptr) { - return savedata::SD_ERROR_PARAMETER; - } - const int rc = savedata::set_memory_internal(set_param); - SAVEDATA_TRACE("set2 user=%d slot=%u num=%u -> 0x%08x", - static_cast(set_param->user_id), static_cast(set_param->slot_id), - static_cast(set_param->data_num), static_cast(rc)); - return rc; -} -int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, - SaveDataMemorySetupResult* result) { - const int rc = savedata::setup_memory_internal(setup_param, result, nullptr); - SAVEDATA_TRACE("setup2 -> 0x%08x existed=0x%zx", static_cast(rc), - result != nullptr ? result->existed_memory_size : static_cast(0)); - return rc; -} -int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { - if (sync_param == nullptr) { - return savedata::SD_ERROR_PARAMETER; - } - const auto* sync = static_cast(sync_param); - const int rc = savedata::sync_memory_internal(sync->user_id, sync->slot_id, sync->option, nullptr, true); - SAVEDATA_TRACE("sync user=%d slot=%u option=%u -> 0x%08x", static_cast(sync->user_id), - static_cast(sync->slot_id), static_cast(sync->option), - static_cast(rc)); - return rc; -} -int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* mount, - SaveDataMountResult* mount_result) { - const int rc = savedata::transferring_mount_internal(mount, mount_result); - SAVEDATA_TRACE("sceSaveDataTransferringMount -> 0x%08x", static_cast(rc)); - return rc; -} -} diff --git a/core/libs/prx/libSceSaveData/SaveDataCore.hpp b/core/libs/prx/libSceSaveData/SaveDataCore.hpp deleted file mode 100644 index 4705d8d62e..0000000000 --- a/core/libs/prx/libSceSaveData/SaveDataCore.hpp +++ /dev/null @@ -1,1326 +0,0 @@ -#ifndef CORE_LIBS_PRX_LIBSCESAVEDATA_SAVEDATACORE_HPP -#define CORE_LIBS_PRX_LIBSCESAVEDATA_SAVEDATACORE_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" -#include "prx/libSceSaveData/SaveDataFile.hpp" - -namespace savedata { - -extern "C" { -int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, - std::uint64_t init_pattern, const void* param); -int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); -int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); -int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); -} - -namespace { - -constexpr std::uint32_t SD_FACILITY = 0x809F0000u; -constexpr int sd_error(std::uint32_t code) { return static_cast(SD_FACILITY + code); } - -constexpr int SD_OK = 0; -constexpr int SD_ERROR_PARAMETER = sd_error(0x00); -constexpr int SD_ERROR_NOT_INITIALIZED = sd_error(0x01); -constexpr int SD_ERROR_OUT_OF_MEMORY = sd_error(0x02); -constexpr int SD_ERROR_BUSY = sd_error(0x03); -constexpr int SD_ERROR_NOT_MOUNTED = sd_error(0x04); -constexpr int SD_ERROR_NO_PERMISSION = sd_error(0x05); -constexpr int SD_ERROR_FINGERPRINT_MISMATCH = sd_error(0x06); -constexpr int SD_ERROR_EXISTS = sd_error(0x07); -constexpr int SD_ERROR_NOT_FOUND = sd_error(0x08); -constexpr int SD_ERROR_NO_SPACE_FS = sd_error(0x0A); -constexpr int SD_ERROR_INTERNAL = sd_error(0x0B); -constexpr int SD_ERROR_MOUNT_FULL = sd_error(0x0C); -constexpr int SD_ERROR_BAD_MOUNTED = sd_error(0x0D); -constexpr int SD_ERROR_FILE_NOT_FOUND = sd_error(0x0E); -constexpr int SD_ERROR_BROKEN = sd_error(0x0F); -constexpr int SD_ERROR_INVALID_LOGIN_USER = sd_error(0x11); -constexpr int SD_ERROR_MEMORY_NOT_READY = sd_error(0x12); -constexpr int SD_ERROR_BACKUP_BUSY = sd_error(0x13); -constexpr int SD_ERROR_NOT_REGIST_CALLBACK = sd_error(0x15); -constexpr int SD_ERROR_BUSY_FOR_SAVING = sd_error(0x16); -constexpr int SD_ERROR_LIMITATION_OVER = sd_error(0x17); -constexpr int SD_ERROR_EVENT_BUSY = sd_error(0x18); - -constexpr std::uint32_t SD_MOUNT_RDONLY = 1u; -constexpr std::uint32_t SD_MOUNT_RDWR = 2u; -constexpr std::uint32_t SD_MOUNT_CREATE = 4u; -constexpr std::uint32_t SD_MOUNT_COPY_ICON = 16u; -constexpr std::uint32_t SD_MOUNT_CREATE2 = 32u; - -constexpr std::uint32_t SD_UMOUNT_BACKUP_ASYNC = 1u << 16u; -constexpr std::uint32_t SD_COMMIT_BACKUP_ASYNC = 1u; - -constexpr std::uint32_t SD_EVENT_UMOUNT_BACKUP_END = 1u; -constexpr std::uint32_t SD_EVENT_BACKUP_END = 2u; -constexpr std::uint32_t SD_EVENT_MEMORY_SYNC_END = 3u; -constexpr std::uint32_t SD_EVENT_COMMIT_BACKUP_END = 4u; - -constexpr std::uint64_t SD_BLOCKS_MIN = 1; -constexpr std::uint64_t SD_BLOCKS_MAX = 32768; - -constexpr std::uint32_t SD_MEMORY_SLOTS = 4; -constexpr std::uint32_t SD_MEMORY_MAX_MOUNTED = 4; -constexpr std::size_t SD_MEMORY_MAX_SIZE = 32u * 1024u * 1024u; -constexpr std::uint32_t SD_MEMORY_SET_PARAM = 1u; -constexpr std::uint32_t SD_MEMORY_DOUBLE_BUFFER = 2u; -constexpr std::uint32_t SD_MEMORY_MAX_DATA_NUM = 5; - -constexpr std::uint32_t SD_PARAM_WHOLE = 0; -constexpr std::uint32_t SD_PARAM_TITLE = 1; -constexpr std::uint32_t SD_PARAM_SUB_TITLE = 2; -constexpr std::uint32_t SD_PARAM_DETAIL = 3; -constexpr std::uint32_t SD_PARAM_USER_PARAM = 4; -constexpr std::uint32_t SD_PARAM_MTIME = 5; - -constexpr std::uint32_t SD_APP_STATUS_NONE = 0; -constexpr std::uint32_t SD_APP_STATUS_DELETED = 1; -constexpr std::uint32_t SD_APP_STATUS_LOCAL = 2; -constexpr std::uint32_t SD_APP_STATUS_ONLINE = 4; -constexpr std::uint32_t SD_APP_STATUS_ANY = 0x7FFFFFFFu; - -constexpr char SD_ROOT_GUEST[] = "/_sd"; -constexpr char SD_SUBDIR[] = "sce_sys"; -constexpr char SD_MEMORY_PREFIX[] = "sce_sdmemory"; -constexpr std::size_t SD_MOUNT_PREFIX_LEN = sizeof(SD_ROOT_GUEST); -constexpr std::size_t SD_MOUNT_POINT_CAPACITY = sizeof(SaveDataMountPoint) - SD_MOUNT_PREFIX_LEN - 1; - -struct MemorySync { - std::int32_t user_id; - std::uint32_t slot_id; - std::uint32_t option; - std::uint8_t reserved[28]; -}; -static_assert(sizeof(MemorySync) == 40, "SceSaveDataMemorySync is 40 bytes"); - -struct EventParam { - std::size_t size; - std::uint8_t reserved[24]; -}; - -struct MountSlot { - bool used = false; - std::string mount_point; - std::string dir_name; - std::filesystem::path directory; -}; - -struct MemorySlot { - std::vector data; - SaveDataParam param {}; - std::uint32_t option = 0; - bool dirty = false; -}; - -struct PendingEvent { - SaveDataEvent record {}; - KernelEventFlag flag = nullptr; -}; - -struct State { - std::mutex mutex; - std::uint32_t instances = 0; - std::array slots {}; - std::deque events; - std::unordered_map memory; - std::map aliases; - std::int32_t next_transaction_resource = 1; - std::set live_transaction_resources; - void* event_callback = nullptr; - void* event_callback_userdata = nullptr; - bool aliases_loaded = false; -}; - -State& state() { - static State instance; - return instance; -} - -bool is_ready() { - return state().instances != 0; -} - -bool valid_dir_name(const char* name) { - if (name == nullptr || name[0] == '\0') { - return false; - } - std::size_t length = 0; - while (name[length] != '\0') { - length++; - if (length >= sizeof(SceSaveDataDirName::data)) { - return false; - } - } - if (std::strcmp(name, ".") == 0 || std::strcmp(name, "..") == 0) { - return false; - } - for (const char* p = name; *p != '\0'; p++) { - const bool ok = std::isalnum(static_cast(*p)) != 0 || *p == '_' || *p == '-' || - *p == '.' || *p == '@'; - if (!ok) { - return false; - } - } - return true; -} - -bool bounded_string_ok(const char* data, std::size_t capacity) { - return std::memchr(data, '\0', capacity) != nullptr; -} - -const std::filesystem::path& save_root() { - static const std::filesystem::path root = ResolvePath_nid_no_patch(SD_ROOT_GUEST); - return root; -} - -bool ensure_save_root(std::error_code& error) { - if (std::filesystem::create_directories(save_root(), error)) RecordWrittenPath_nid_no_patch(save_root()); - if (error) { - error.clear(); - } - return std::filesystem::is_directory(save_root(), error); -} - -void load_aliases_locked(State& st) { - if (st.aliases_loaded) { - return; - } - st.aliases_loaded = true; - std::ifstream file(save_root() / "_aliases", std::ios::binary); - std::string line; - while (std::getline(file, line)) { - const std::size_t sep = line.find('\t'); - if (sep == std::string::npos || sep == 0) { - continue; - } - st.aliases[line.substr(0, sep)] = line.substr(sep + 1); - } -} - -void store_aliases_locked(State& st) { - std::error_code error; - ensure_save_root(error); - std::ofstream file(save_root() / "_aliases", std::ios::binary | std::ios::trunc); - for (const auto& [alias, dir_name] : st.aliases) { - file << alias << '\t' << dir_name << '\n'; - } - RecordWrittenPath_nid_no_patch(save_root() / "_aliases"); -} - -std::string save_alias_locked(State& st, const std::string& dir_name) { - if (dir_name.size() <= SD_MOUNT_POINT_CAPACITY) { - return dir_name; - } - load_aliases_locked(st); - const std::string base = dir_name.substr(0, 6); - std::uint32_t hash = 2166136261u; - for (const char c : dir_name) { - hash = (hash ^ static_cast(c)) * 16777619u; - } - for (std::uint32_t attempt = 0; attempt < 4096u; attempt++) { - char tail[8]; - std::snprintf(tail, sizeof(tail), "~%03X", static_cast((hash + attempt) & 0xFFFu)); - const std::string candidate = base + tail; - const auto found = st.aliases.find(candidate); - if (found == st.aliases.end()) { - st.aliases[candidate] = dir_name; - store_aliases_locked(st); - return candidate; - } - if (found->second == dir_name) { - return candidate; - } - } - return base; -} - -std::string real_dir_name_locked(State& st, const std::string& alias) { - load_aliases_locked(st); - const auto found = st.aliases.find(alias); - return found != st.aliases.end() ? found->second : alias; -} - -std::string mount_point_for(const std::string& alias) { - return std::string(SD_ROOT_GUEST) + "/" + alias; -} - -std::filesystem::path save_directory(const std::string& alias) { - return save_root() / alias; -} - -std::filesystem::path save_metadata_directory(const std::filesystem::path& directory) { - return directory / SD_SUBDIR; -} - -std::filesystem::path save_param_path(const std::filesystem::path& directory) { - return save_metadata_directory(directory) / "param.bin"; -} - -std::filesystem::path save_blocks_path(const std::filesystem::path& directory) { - return save_metadata_directory(directory) / "blocks.bin"; -} - -bool write_blob(const std::filesystem::path& path, const void* data, std::size_t size) { - if (!replace_file(path, data, size)) return false; - RecordWrittenPath_nid_no_patch(path); - return true; -} - -bool read_blob(const std::filesystem::path& path, void* data, std::size_t size) { - std::ifstream file(path, std::ios::binary); - if (!file) { - return false; - } - file.read(static_cast(data), static_cast(size)); - return static_cast(file.gcount()) == size; -} - -int write_u64(const std::filesystem::path& path, std::uint64_t value) { - return write_blob(path, &value, sizeof(value)) ? SD_OK : SD_ERROR_INTERNAL; -} - -int read_u64(const std::filesystem::path& path, std::uint64_t* value) { - std::error_code error; - if (!std::filesystem::is_regular_file(path, error) || error) { - return SD_ERROR_BROKEN; - } - if (std::filesystem::file_size(path, error) != sizeof(std::uint64_t) || error) { - return SD_ERROR_BROKEN; - } - return read_blob(path, value, sizeof(*value)) ? SD_OK : SD_ERROR_BROKEN; -} - -int load_save_param(const std::filesystem::path& directory, SaveDataParam* param) { - std::memset(param, 0, sizeof(*param)); - const int status = read_blob(save_param_path(directory), param, sizeof(*param)) ? SD_OK : SD_ERROR_NOT_FOUND; - if (status == SD_OK) { - std::int64_t newest = 0; - std::error_code error; - for (std::filesystem::recursive_directory_iterator it(directory, - std::filesystem::directory_options::skip_permission_denied, error), end; - it != end; it.increment(error)) { - if (error) { - error.clear(); - continue; - } - std::error_code file_error; - if (it->is_regular_file(file_error) && !file_error) { - const auto written = std::filesystem::last_write_time(it->path(), file_error); - if (!file_error) { - const auto seconds = std::chrono::duration_cast( - std::filesystem::file_time_type::clock::now() - written).count(); - const auto now = static_cast(std::time(nullptr)); - newest = std::max(newest, now - static_cast(seconds)); - } - } - } - param->mtime = newest; - } - return status; -} - -bool save_is_mounted_locked(const State& st, const std::filesystem::path& directory) { - for (const auto& slot : st.slots) { - if (slot.used && slot.directory == directory) { - return true; - } - } - return false; -} - -int slot_of_mount_point_locked(const State& st, const char* mount_point) { - for (std::size_t i = 0; i < st.slots.size(); i++) { - if (st.slots[i].used && st.slots[i].mount_point == mount_point) { - return static_cast(i); - } - } - return -1; -} - -int free_slot_locked(const State& st) { - for (std::size_t i = 0; i < st.slots.size(); i++) { - if (!st.slots[i].used) { - return static_cast(i); - } - } - return -1; -} - -void queue_event_locked(State& st, std::uint32_t type, std::int32_t user_id, - const SceSaveDataTitleId* title_id, const SceSaveDataDirName* dir_name, - int error_code, KernelEventFlag flag) { - PendingEvent pending; - pending.record = {}; - pending.record.type = type; - pending.record.error_code = error_code; - pending.record.user_id = user_id; - if (title_id != nullptr) { - std::memcpy(&pending.record.title_id, title_id, sizeof(pending.record.title_id)); - } - if (dir_name != nullptr) { - std::memcpy(&pending.record.dir_name, dir_name, sizeof(pending.record.dir_name)); - } - pending.flag = flag; - st.events.push_back(pending); - while (st.events.size() > 64) { - if (st.events.front().flag != nullptr) { - sceKernelDeleteEventFlag(st.events.front().flag); - } - st.events.pop_front(); - } -} - -KernelEventFlag create_signalled_flag(const char* name) { - KernelEventFlag flag = nullptr; - if (sceKernelCreateEventFlag(&flag, name, 0x20u, 0, nullptr) != 0) { - return nullptr; - } - sceKernelSetEventFlag(flag, 1u); - return flag; -} - -int initialize_internal(const void* init) { - State& st = state(); - std::lock_guard lock(st.mutex); - st.instances++; - return SD_OK; -} - -int terminate_internal() { - State& st = state(); - std::lock_guard lock(st.mutex); - if (st.instances == 0) { - return SD_ERROR_NOT_INITIALIZED; - } - if (std::any_of(st.slots.begin(), st.slots.end(), - [](const MountSlot& slot) { return slot.used; }) || !st.memory.empty()) { - return SD_ERROR_BUSY; - } - st.instances--; - if (st.instances == 0) { - for (auto& pending : st.events) { - if (pending.flag != nullptr) { - sceKernelDeleteEventFlag(pending.flag); - } - } - st.events.clear(); - st.memory.clear(); - st.live_transaction_resources.clear(); - } - return SD_OK; -} - -int mount_internal(const SceSaveDataDirName* dir_name, std::uint32_t mount_mode, - std::uint64_t blocks, std::int32_t user_id, SaveDataMountResult* mount_result, - const char* api) { - if (mount_result == nullptr) { - return SD_ERROR_PARAMETER; - } - std::memset(mount_result, 0, sizeof(*mount_result)); - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || - !valid_dir_name(dir_name->data)) { - return SD_ERROR_PARAMETER; - } - if (user_id < 0) { - return SD_ERROR_INVALID_LOGIN_USER; - } - const bool create = (mount_mode & SD_MOUNT_CREATE) != 0; - const bool create_or_open = (mount_mode & SD_MOUNT_CREATE2) != 0; - const bool open = !create && !create_or_open && (mount_mode & (SD_MOUNT_RDONLY | SD_MOUNT_RDWR)) != 0; - if (!create && !create_or_open && !open) { - return SD_ERROR_PARAMETER; - } - - State& st = state(); - std::lock_guard lock(st.mutex); - const std::string requested = dir_name->data; - const std::string alias = save_alias_locked(st, requested); - const std::string mount_point = mount_point_for(alias); - if (mount_point.size() + 1 > sizeof(mount_result->mount_point)) { - return SD_ERROR_PARAMETER; - } - const std::filesystem::path directory = save_directory(alias); - for (const auto& slot : st.slots) { - if (slot.used && slot.directory == directory) { - return SD_ERROR_BUSY; - } - } - const int free_index = free_slot_locked(st); - if (free_index == -1) { - return SD_ERROR_MOUNT_FULL; - } - - std::error_code error; - const bool exists = std::filesystem::is_directory(directory, error) && !error; - if (open && !exists) { - return SD_ERROR_NOT_FOUND; - } - if (create && exists) { - return SD_ERROR_EXISTS; - } - bool created = false; - if ((create || create_or_open) && !exists) { - if (blocks < SD_BLOCKS_MIN || blocks > SD_BLOCKS_MAX) { - return SD_ERROR_PARAMETER; - } - std::error_code root_error; - if (!ensure_save_root(root_error)) { - return SD_ERROR_INTERNAL; - } - if (!std::filesystem::create_directories(directory, error) || error) { - return SD_ERROR_INTERNAL; - } - RecordWrittenPath_nid_no_patch(directory); - created = true; - if (write_u64(save_blocks_path(directory), blocks) != SD_OK) { - std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); - return SD_ERROR_INTERNAL; - } - SaveDataParam initial {}; - std::strncpy(initial.title, "Saved Data", sizeof(initial.title) - 1); - if (!write_blob(save_param_path(directory), &initial, sizeof(initial))) { - std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); - return SD_ERROR_INTERNAL; - } - } - if (exists && !created) { - std::uint64_t recorded = 0; - if (read_u64(save_blocks_path(directory), &recorded) != SD_OK && blocks != 0) { - if (blocks < SD_BLOCKS_MIN || blocks > SD_BLOCKS_MAX) { - return SD_ERROR_PARAMETER; - } - write_u64(save_blocks_path(directory), blocks); - } - } - - MountSlot& slot = st.slots[static_cast(free_index)]; - slot.used = true; - slot.mount_point = mount_point; - slot.dir_name = requested; - slot.directory = directory; - std::memcpy(mount_result->mount_point.data, mount_point.c_str(), mount_point.size() + 1); - mount_result->required_blocks = 0; - mount_result->mount_status = created ? 1u : 0u; - return SD_OK; -} - -int umount_internal(std::uint32_t mode, const SaveDataMountPoint* mount_point, const char* api) { - State& st = state(); - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (mount_point == nullptr || !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { - return SD_ERROR_PARAMETER; - } - std::lock_guard lock(st.mutex); - const int index = slot_of_mount_point_locked(st, mount_point->data); - if (index == -1) { - return SD_ERROR_NOT_MOUNTED; - } - st.slots[static_cast(index)] = MountSlot {}; - if ((mode & SD_UMOUNT_BACKUP_ASYNC) != 0) { - queue_event_locked(st, SD_EVENT_UMOUNT_BACKUP_END, 0, nullptr, nullptr, SD_OK, nullptr); - } - return SD_OK; -} - -int delete_internal(std::int32_t user_id, const SceSaveDataDirName* dir_name, - const SceSaveDataTitleId* title_id) { - State& st = state(); - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || - !valid_dir_name(dir_name->data) || - (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data)))) { - return SD_ERROR_PARAMETER; - } - if (user_id < 0) { - return SD_ERROR_INVALID_LOGIN_USER; - } - std::lock_guard lock(st.mutex); - const std::string alias = save_alias_locked(st, dir_name->data); - const std::filesystem::path directory = save_directory(alias); - if (save_is_mounted_locked(st, directory)) { - return SD_ERROR_BUSY; - } - std::error_code error; - if (!std::filesystem::exists(directory, error) || error) { - return SD_ERROR_NOT_FOUND; - } - std::filesystem::remove_all(directory, error); - RecordWrittenPath_nid_no_patch(directory); - if (error) { - return SD_ERROR_INTERNAL; - } - st.aliases.erase(alias); - if (alias != dir_name->data) { - store_aliases_locked(st); - } - return SD_OK; -} - -bool dir_name_matches(const char* str, const char* pattern) { - if (pattern == nullptr || pattern[0] == '\0') { - return true; - } - while (*str != '\0' && *pattern != '\0') { - if (*pattern == '%') { - for (const char* s = str;; s++) { - if (dir_name_matches(s, pattern + 1)) { - return true; - } - if (*s == '\0') { - break; - } - } - return false; - } - if (*pattern == '_') { - str++; - pattern++; - continue; - } - if (std::tolower(static_cast(*str)) != - std::tolower(static_cast(*pattern))) { - return false; - } - str++; - pattern++; - } - return *str == '\0' && *pattern == '\0'; -} - -int dir_name_search_internal(const SaveDataDirNameSearchCond* cond, - SaveDataDirNameSearchResult* result) { - State& st = state(); - if (result != nullptr) { - std::memset(result, 0, sizeof(*result)); - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (cond == nullptr || result == nullptr || - (result->dir_names_num != 0 && result->dir_names == nullptr) || - (cond->dir_name != nullptr && !bounded_string_ok(cond->dir_name->data, sizeof(cond->dir_name->data))) || - (cond->title_id != nullptr && !bounded_string_ok(cond->title_id->data, sizeof(cond->title_id->data)))) { - return SD_ERROR_PARAMETER; - } - if (cond->user_id < 0) { - return SD_ERROR_INVALID_LOGIN_USER; - } - std::lock_guard lock(st.mutex); - load_aliases_locked(st); - std::vector found; - std::error_code error; - if (std::filesystem::is_directory(save_root(), error) && !error) { - for (const auto& entry : std::filesystem::directory_iterator(save_root(), error)) { - if (error) { - break; - } - std::error_code kind_error; - if (!entry.is_directory(kind_error) || kind_error) { - continue; - } - const std::string alias = entry.path().filename().string(); - if (alias.empty() || alias[0] == '.' || alias[0] == '_' || - !valid_dir_name(alias.c_str())) { - continue; - } - std::error_code meta_error; - if (!std::filesystem::is_regular_file(save_param_path(entry.path()), meta_error) || meta_error) { - continue; - } - const std::string dir_name = real_dir_name_locked(st, alias); - if (cond->dir_name != nullptr && !dir_name_matches(dir_name.c_str(), cond->dir_name->data)) { - continue; - } - found.push_back(dir_name); - } - } - std::sort(found.begin(), found.end()); - if (cond->order == 1) { - std::reverse(found.begin(), found.end()); - } - result->hit_num = static_cast(found.size()); - const std::uint32_t set_count = std::min(result->dir_names_num, found.size()); - for (std::uint32_t i = 0; i < set_count; i++) { - std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s", - found[i].c_str()); - if (result->params != nullptr) { - load_save_param(save_directory(save_alias_locked(st, found[i])), &result->params[i]); - } - if (result->infos != nullptr) { - std::memset(&result->infos[i], 0, sizeof(result->infos[i])); - std::uint64_t blocks = 0; - if (read_u64(save_blocks_path(save_directory(save_alias_locked(st, found[i]))), &blocks) == SD_OK) { - std::memcpy(&result->infos[i], &blocks, sizeof(blocks)); - } - } - } - result->set_num = set_count; - return SD_OK; -} - -int get_param_internal(const SaveDataMountPoint* mount_point, std::uint32_t param_type, - void* param_buf, std::size_t param_buf_size, std::size_t* got_size) { - State& st = state(); - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (mount_point == nullptr || param_buf == nullptr || param_type > SD_PARAM_MTIME || - !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { - return SD_ERROR_PARAMETER; - } - std::lock_guard lock(st.mutex); - const int index = slot_of_mount_point_locked(st, mount_point->data); - if (index == -1) { - return SD_ERROR_NOT_MOUNTED; - } - SaveDataParam param {}; - const int status = load_save_param(st.slots[static_cast(index)].directory, ¶m); - if (status != SD_OK) { - return status; - } - std::size_t written = 0; - if (param_type == SD_PARAM_WHOLE) { - if (param_buf_size < sizeof(param)) { - return SD_ERROR_PARAMETER; - } - std::memcpy(param_buf, ¶m, sizeof(param)); - written = sizeof(param); - } else if (param_type == SD_PARAM_USER_PARAM || param_type == SD_PARAM_MTIME) { - const std::size_t size = param_type == SD_PARAM_USER_PARAM ? sizeof(param.user_param) - : sizeof(param.mtime); - if (param_buf_size < size) { - return SD_ERROR_PARAMETER; - } - std::memcpy(param_buf, - param_type == SD_PARAM_USER_PARAM - ? static_cast(¶m.user_param) - : static_cast(¶m.mtime), - size); - written = size; - } else { - char* field = param_type == SD_PARAM_TITLE ? param.title - : param_type == SD_PARAM_SUB_TITLE ? param.sub_title : param.detail; - const std::size_t capacity = param_type == SD_PARAM_DETAIL ? sizeof(param.detail) - : sizeof(param.title); - if (param_buf_size == 0) { - return SD_ERROR_PARAMETER; - } - const std::size_t length = std::min(std::strlen(field), param_buf_size - 1); - std::memcpy(param_buf, field, length); - static_cast(param_buf)[length] = '\0'; - written = length + 1; - } - if (got_size != nullptr) { - *got_size = written; - } - return SD_OK; -} - -int set_param_internal(const SaveDataMountPoint* mount_point, std::uint32_t param_type, - const void* param_buf, std::size_t param_buf_size) { - State& st = state(); - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (mount_point == nullptr || param_buf == nullptr || param_type > SD_PARAM_MTIME || - !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { - return SD_ERROR_PARAMETER; - } - std::lock_guard lock(st.mutex); - const int index = slot_of_mount_point_locked(st, mount_point->data); - if (index == -1) { - return SD_ERROR_NOT_MOUNTED; - } - const std::filesystem::path directory = st.slots[static_cast(index)].directory; - if (param_type == SD_PARAM_WHOLE) { - if (param_buf_size < sizeof(SaveDataParam)) { - return SD_ERROR_PARAMETER; - } - SaveDataParam param = *static_cast(param_buf); - param.mtime = static_cast(std::time(nullptr)); - return write_blob(save_param_path(directory), ¶m, sizeof(param)) ? SD_OK : SD_ERROR_INTERNAL; - } - SaveDataParam param {}; - const int status = load_save_param(directory, ¶m); - if (status != SD_OK && status != SD_ERROR_NOT_FOUND) { - return status; - } - if (param_type == SD_PARAM_USER_PARAM || param_type == SD_PARAM_MTIME) { - const std::size_t size = param_type == SD_PARAM_USER_PARAM ? sizeof(param.user_param) - : sizeof(param.mtime); - if (param_buf_size < size) { - return SD_ERROR_PARAMETER; - } - std::memcpy(param_type == SD_PARAM_USER_PARAM ? static_cast(¶m.user_param) - : static_cast(¶m.mtime), - param_buf, size); - } else { - char* field = param_type == SD_PARAM_TITLE ? param.title - : param_type == SD_PARAM_SUB_TITLE ? param.sub_title : param.detail; - const std::size_t capacity = param_type == SD_PARAM_DETAIL ? sizeof(param.detail) - : sizeof(param.title); - if (param_buf_size == 0) { - return SD_ERROR_PARAMETER; - } - const std::size_t length = std::min(param_buf_size, capacity - 1); - const char* text = static_cast(param_buf); - const char* end = static_cast(std::memchr(text, '\0', length)); - std::memset(field, 0, capacity); - std::memcpy(field, text, end != nullptr ? static_cast(end - text) : length); - } - return write_blob(save_param_path(directory), ¶m, sizeof(param)) ? SD_OK : SD_ERROR_INTERNAL; -} - -int get_mount_info_internal(const SaveDataMountPoint* mount_point, SaveDataMountInfo* info) { - State& st = state(); - if (info != nullptr) { - std::memset(info, 0, sizeof(*info)); - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (mount_point == nullptr || info == nullptr || - !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { - return SD_ERROR_PARAMETER; - } - std::lock_guard lock(st.mutex); - const int index = slot_of_mount_point_locked(st, mount_point->data); - if (index == -1) { - return SD_ERROR_NOT_MOUNTED; - } - std::uint64_t blocks = 0; - const int status = read_u64(save_blocks_path(st.slots[static_cast(index)].directory), - &blocks); - if (status != SD_OK) { - return status; - } - info->blocks = blocks; - info->free_blocks = blocks; - return SD_OK; -} - -int is_mounted_internal(const SaveDataMountPoint* mount_point, std::uint32_t* is_mounted) { - if (mount_point == nullptr || is_mounted == nullptr || - !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { - return SD_ERROR_PARAMETER; - } - State& st = state(); - std::lock_guard lock(st.mutex); - *is_mounted = slot_of_mount_point_locked(st, mount_point->data) != -1 ? 1u : 0u; - return SD_OK; -} - -std::int64_t count_mounted_internal() { - State& st = state(); - std::lock_guard lock(st.mutex); - std::int64_t count = 0; - for (const auto& slot : st.slots) { - if (slot.used) { - count++; - } - } - return count; -} - -std::string memory_key(std::int32_t user_id, std::uint32_t slot_id) { - char key[32]; - std::snprintf(key, sizeof(key), "%d:%u", static_cast(user_id), static_cast(slot_id)); - return key; -} - -std::filesystem::path memory_directory(std::int32_t user_id, std::uint32_t slot_id) { - char name[48]; - std::snprintf(name, sizeof(name), "%s%s", SD_MEMORY_PREFIX, - slot_id == 0 ? "" : std::to_string(slot_id).c_str()); - return save_root() / name / std::to_string(static_cast(user_id)); -} - -bool valid_memory_range(const SaveDataMemoryData& data, std::size_t size) { - return data.buf != nullptr && data.offset <= size && data.buf_size <= size - data.offset; -} - -int setup_memory_internal(const SaveDataMemorySetup2* setup, SaveDataMemorySetupResult* result, - KernelEventFlag flag) { - State& st = state(); - if (setup == nullptr || setup->slot_id >= SD_MEMORY_SLOTS || setup->memory_size == 0 || - setup->memory_size > SD_MEMORY_MAX_SIZE || (setup->option & ~(SD_MEMORY_SET_PARAM | SD_MEMORY_DOUBLE_BUFFER)) != 0 || - (setup->init_param != nullptr && setup->option != 0 && (setup->option & SD_MEMORY_SET_PARAM) == 0)) { - return SD_ERROR_PARAMETER; - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (setup->user_id < 0) { - return SD_ERROR_INVALID_LOGIN_USER; - } - std::lock_guard lock(st.mutex); - const std::string key = memory_key(setup->user_id, setup->slot_id); - if (st.memory.count(key) != 0) { - return SD_ERROR_BUSY; - } - if (st.memory.size() >= SD_MEMORY_MAX_MOUNTED) { - return SD_ERROR_LIMITATION_OVER; - } - if (save_is_mounted_locked(st, memory_directory(setup->user_id, setup->slot_id))) { - return SD_ERROR_BUSY; - } - MemorySlot slot; - slot.option = setup->option; - slot.data.resize(setup->memory_size, 0); - std::strncpy(slot.param.title, "Saved Data", sizeof(slot.param.title) - 1); - std::size_t existed = 0; - std::error_code error; - const std::filesystem::path blob = memory_directory(setup->user_id, setup->slot_id) / "memory.dat"; - if (std::filesystem::is_regular_file(blob, error) && !error) { - const std::size_t stored = static_cast(std::filesystem::file_size(blob, error)); - if (error || stored > SD_MEMORY_MAX_SIZE) { - return SD_ERROR_BROKEN; - } - existed = stored; - std::vector loaded(stored, 0); - std::ifstream file(blob, std::ios::binary); - file.read(loaded.data(), static_cast(stored)); - if (!file) { - return SD_ERROR_BROKEN; - } - std::copy_n(loaded.begin(), std::min(stored, slot.data.size()), slot.data.begin()); - load_save_param(memory_directory(setup->user_id, setup->slot_id), &slot.param); - } - if (existed == 0 && setup->init_param != nullptr && (setup->option & SD_MEMORY_SET_PARAM) != 0) { - slot.param = *setup->init_param; - slot.param.mtime = 0; - } - st.memory.emplace(key, std::move(slot)); - if (result != nullptr) { - std::memset(result, 0, sizeof(*result)); - result->existed_memory_size = existed; - } - if (flag != nullptr) { - queue_event_locked(st, SD_EVENT_MEMORY_SYNC_END, setup->user_id, nullptr, nullptr, SD_OK, flag); - } - return SD_OK; -} - -int get_memory_internal(SaveDataMemoryGet2* get_param) { - State& st = state(); - if (get_param == nullptr || get_param->slot_id >= SD_MEMORY_SLOTS) { - return SD_ERROR_PARAMETER; - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (get_param->user_id < 0) { - return SD_ERROR_INVALID_LOGIN_USER; - } - std::lock_guard lock(st.mutex); - const auto it = st.memory.find(memory_key(get_param->user_id, get_param->slot_id)); - if (it == st.memory.end()) { - return SD_ERROR_MEMORY_NOT_READY; - } - const MemorySlot& slot = it->second; - if (get_param->data != nullptr && - !valid_memory_range(*get_param->data, slot.data.size())) { - return SD_ERROR_PARAMETER; - } - if (get_param->param != nullptr && (slot.option & SD_MEMORY_SET_PARAM) == 0) { - return SD_ERROR_PARAMETER; - } - if (get_param->data != nullptr) { - const SaveDataMemoryData& data = *get_param->data; - std::memcpy(data.buf, slot.data.data() + data.offset, data.buf_size); - } - if (get_param->param != nullptr) { - *get_param->param = slot.param; - } - if (get_param->icon != nullptr) { - get_param->icon->data_size = 0; - } - return SD_OK; -} - -int set_memory_internal(const SaveDataMemorySet2* set_param) { - State& st = state(); - if (set_param == nullptr || set_param->slot_id >= SD_MEMORY_SLOTS || - set_param->data_num > SD_MEMORY_MAX_DATA_NUM || - (set_param->data == nullptr && set_param->data_num != 0)) { - return SD_ERROR_PARAMETER; - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (set_param->user_id < 0) { - return SD_ERROR_INVALID_LOGIN_USER; - } - std::lock_guard lock(st.mutex); - const auto it = st.memory.find(memory_key(set_param->user_id, set_param->slot_id)); - if (it == st.memory.end()) { - return SD_ERROR_MEMORY_NOT_READY; - } - if (set_param->param != nullptr && (it->second.option & SD_MEMORY_SET_PARAM) == 0) { - return SD_ERROR_PARAMETER; - } - const std::uint32_t count = - set_param->data != nullptr && set_param->data_num == 0 ? 1u : set_param->data_num; - const SaveDataMemoryData* descriptors = set_param->data; - for (std::uint32_t i = 0; i < count; i++) { - if (!valid_memory_range(descriptors[i], it->second.data.size())) { - return SD_ERROR_PARAMETER; - } - } - MemorySlot updated = it->second; - for (std::uint32_t i = 0; i < count; i++) { - const SaveDataMemoryData& data = descriptors[i]; - std::memcpy(updated.data.data() + data.offset, data.buf, data.buf_size); - } - if (set_param->param != nullptr) { - updated.param = *set_param->param; - } - updated.param.mtime = static_cast(std::time(nullptr)); - updated.dirty = true; - it->second = std::move(updated); - return SD_OK; -} - -int sync_memory_internal(const std::int32_t user_id, const std::uint32_t slot_id, - const std::uint32_t option, KernelEventFlag flag, bool report_event) { - State& st = state(); - if (slot_id >= SD_MEMORY_SLOTS || option > 1) { - return SD_ERROR_PARAMETER; - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (user_id < 0) { - return SD_ERROR_INVALID_LOGIN_USER; - } - std::lock_guard lock(st.mutex); - const auto it = st.memory.find(memory_key(user_id, slot_id)); - if (it == st.memory.end()) { - return SD_ERROR_MEMORY_NOT_READY; - } - const std::filesystem::path directory = memory_directory(user_id, slot_id); - int status = SD_OK; - if (!write_blob(directory / "memory.dat", it->second.data.data(), it->second.data.size())) { - status = SD_ERROR_INTERNAL; - } else if (!write_blob(save_param_path(directory), &it->second.param, sizeof(it->second.param))) { - status = SD_ERROR_INTERNAL; - } else { - it->second.dirty = false; - } - if (option == 1 || !report_event) { - return status; - } - char title_text[sizeof(SceSaveDataTitleId)] = {}; - char dir_text[sizeof(SceSaveDataDirName)] = {}; - std::snprintf(dir_text, sizeof(dir_text), "%s%s", SD_MEMORY_PREFIX, - slot_id == 0 ? "" : std::to_string(slot_id).c_str()); - SceSaveDataTitleId title {}; - SceSaveDataDirName directory_name {}; - std::memcpy(title.data, title_text, sizeof(title.data)); - std::memcpy(directory_name.data, dir_text, sizeof(directory_name.data)); - queue_event_locked(st, SD_EVENT_MEMORY_SYNC_END, user_id, &title, &directory_name, status, flag); - return SD_OK; -} - -int commit_internal(const SaveDataCommitParam* param) { - State& st = state(); - if (param == nullptr) { - return SD_ERROR_PARAMETER; - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - std::lock_guard lock(st.mutex); - for (auto& [key, slot] : st.memory) { - if (!slot.dirty) { - continue; - } - const std::size_t sep = key.find(':'); - const std::int32_t user_id = static_cast(std::strtol(key.c_str(), nullptr, 10)); - const std::uint32_t slot_id = sep == std::string::npos - ? 0u - : static_cast(std::strtoul(key.c_str() + sep + 1, nullptr, 10)); - const std::filesystem::path directory = memory_directory(user_id, slot_id); - if (!write_blob(directory / "memory.dat", slot.data.data(), slot.data.size()) || - !write_blob(save_param_path(directory), &slot.param, sizeof(slot.param))) { - return SD_ERROR_INTERNAL; - } - slot.dirty = false; - } - if ((param->commit_mode & SD_COMMIT_BACKUP_ASYNC) != 0) { - queue_event_locked(st, SD_EVENT_COMMIT_BACKUP_END, 0, nullptr, nullptr, SD_OK, nullptr); - } - return SD_OK; -} - -int prepare_internal(const SaveDataMountPoint* mount_point, const SaveDataPrepareParam* param) { - if (mount_point == nullptr || param == nullptr || - !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { - return SD_ERROR_PARAMETER; - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - return SD_OK; -} - -int backup_internal(const SaveDataBackup* backup, KernelEventFlag flag) { - State& st = state(); - if (backup == nullptr || backup->dir_name == nullptr || - !bounded_string_ok(backup->dir_name->data, sizeof(backup->dir_name->data))) { - return SD_ERROR_PARAMETER; - } - if (backup->title_id != nullptr && !bounded_string_ok(backup->title_id->data, sizeof(backup->title_id->data))) { - return SD_ERROR_PARAMETER; - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (backup->user_id < 0) { - return SD_ERROR_INVALID_LOGIN_USER; - } - std::lock_guard lock(st.mutex); - queue_event_locked(st, SD_EVENT_BACKUP_END, backup->user_id, backup->title_id, backup->dir_name, - SD_OK, flag); - return SD_OK; -} - -int transferring_mount_internal(const SaveDataTransferringMount* mount, - SaveDataMountResult* mount_result) { - if (mount_result != nullptr) { - std::memset(mount_result, 0, sizeof(*mount_result)); - } - State& st = state(); - if (mount == nullptr || mount_result == nullptr || mount->title_id == nullptr || - mount->dir_name == nullptr || - !bounded_string_ok(mount->title_id->data, sizeof(mount->title_id->data)) || - !bounded_string_ok(mount->dir_name->data, sizeof(mount->dir_name->data))) { - return SD_ERROR_PARAMETER; - } - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - if (mount->user_id < 0) { - return SD_ERROR_INVALID_LOGIN_USER; - } - if (!valid_dir_name(mount->title_id->data) || !valid_dir_name(mount->dir_name->data)) { - return SD_ERROR_PARAMETER; - } - std::lock_guard lock(st.mutex); - const std::filesystem::path directory = save_root() / "transfer" / mount->title_id->data / - mount->dir_name->data; - std::error_code error; - if (!std::filesystem::is_directory(directory, error) || error) { - return SD_ERROR_NOT_FOUND; - } - const int index = free_slot_locked(st); - if (index == -1) { - return SD_ERROR_MOUNT_FULL; - } - for (const auto& slot : st.slots) { - if (slot.used && slot.directory == directory) { - return SD_ERROR_BUSY; - } - } - const std::string mount_point = mount_point_for(save_alias_locked( - st, std::string(mount->title_id->data) + "-" + mount->dir_name->data)); - if (mount_point.size() + 1 > sizeof(mount_result->mount_point)) { - return SD_ERROR_PARAMETER; - } - MountSlot& slot = st.slots[static_cast(index)]; - slot.used = true; - slot.mount_point = mount_point; - slot.dir_name = mount->dir_name->data; - slot.directory = directory; - std::memcpy(mount_result->mount_point.data, mount_point.c_str(), mount_point.size() + 1); - mount_result->required_blocks = 0; - mount_result->mount_status = 0; - return SD_OK; -} - -int get_event_result_internal(const void* event_param, SaveDataEvent* event) { - (void)event_param; - State& st = state(); - if (event == nullptr) { - return SD_ERROR_PARAMETER; - } - std::lock_guard lock(st.mutex); - if (st.events.empty()) { - return SD_ERROR_NOT_FOUND; - } - const PendingEvent pending = st.events.front(); - st.events.pop_front(); - if (pending.flag != nullptr) { - sceKernelDeleteEventFlag(pending.flag); - } - *event = pending.record; - return event->error_code; -} - -int cancel_internal(KernelEventFlag event) { - State& st = state(); - if (event == nullptr) { - return SD_ERROR_PARAMETER; - } - std::lock_guard lock(st.mutex); - for (auto it = st.events.begin(); it != st.events.end(); ++it) { - if (it->flag == event) { - sceKernelCancelEventFlag(event, 0, nullptr); - const KernelEventFlag flag = it->flag; - st.events.erase(it); - sceKernelDeleteEventFlag(flag); - return SD_OK; - } - } - return SD_ERROR_NOT_FOUND; -} - -int abort_internal() { - State& st = state(); - std::lock_guard lock(st.mutex); - for (auto& pending : st.events) { - if (pending.flag != nullptr) { - sceKernelCancelEventFlag(pending.flag, 0, nullptr); - sceKernelDeleteEventFlag(pending.flag); - pending.flag = nullptr; - } - } - st.events.clear(); - return SD_OK; -} - -int create_transaction_resource_internal(std::uint32_t size) { - State& st = state(); - std::lock_guard lock(st.mutex); - if (st.next_transaction_resource <= 0) { - return SD_ERROR_OUT_OF_MEMORY; - } - const std::int32_t id = st.next_transaction_resource++; - st.live_transaction_resources.insert(id); - return id; -} - -int delete_transaction_resource_internal(std::int32_t resource) { - State& st = state(); - std::lock_guard lock(st.mutex); - return st.live_transaction_resources.erase(resource) != 0 ? SD_OK : SD_ERROR_PARAMETER; -} - -std::filesystem::path app_status_path(const std::string& dir_name) { - return save_root() / "_app_status" / dir_name; -} - -int get_local_storage_app_status_internal(const SceSaveDataDirName* dir_name, - const SceSaveDataTitleId* title_id, - std::uint32_t* status) { - if (status == nullptr || dir_name == nullptr || - !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || - (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data)))) { - return SD_ERROR_PARAMETER; - } - State& st = state(); - std::lock_guard lock(st.mutex); - *status = SD_APP_STATUS_NONE; - std::error_code error; - const std::filesystem::path path = app_status_path(dir_name->data); - if (std::filesystem::is_regular_file(path, error) && !error) { - std::uint32_t stored = 0; - std::ifstream file(path, std::ios::binary); - file.read(reinterpret_cast(&stored), sizeof(stored)); - if (!file) { - return SD_ERROR_BROKEN; - } - *status = stored; - return SD_OK; - } - const std::string alias = save_alias_locked(st, dir_name->data); - if (std::filesystem::is_directory(save_directory(alias), error) && !error) { - *status = SD_APP_STATUS_LOCAL; - return SD_OK; - } - *status = SD_APP_STATUS_NONE; - return SD_OK; -} - -int set_local_storage_app_status_internal(const SceSaveDataDirName* dir_name, - const SceSaveDataTitleId* title_id, - std::uint32_t status) { - if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || - (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data))) || - (status & ~(SD_APP_STATUS_DELETED | SD_APP_STATUS_LOCAL | SD_APP_STATUS_ONLINE)) != 0) { - return SD_ERROR_PARAMETER; - } - std::error_code error; - if (std::filesystem::create_directories(app_status_path(dir_name->data).parent_path(), error)) - RecordWrittenPath_nid_no_patch(app_status_path(dir_name->data).parent_path()); - if (error) { - return SD_ERROR_INTERNAL; - } - return write_blob(app_status_path(dir_name->data), &status, sizeof(status)) ? SD_OK - : SD_ERROR_INTERNAL; -} - -int delete_local_storage_app_status_internal(const SceSaveDataDirName* dir_name, - const SceSaveDataTitleId* title_id) { - if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || - (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data)))) { - return SD_ERROR_PARAMETER; - } - std::error_code error; - if (std::filesystem::remove(app_status_path(dir_name->data), error)) - RecordWrittenPath_nid_no_patch(app_status_path(dir_name->data)); - return error ? SD_ERROR_INTERNAL : SD_OK; -} - -int check_internal() { - if (!is_ready()) { - return SD_ERROR_NOT_INITIALIZED; - } - std::error_code error; - return ensure_save_root(error) ? SD_OK : SD_ERROR_INTERNAL; -} - -std::uint64_t get_all_size_internal(std::int32_t user_id) { - State& st = state(); - std::lock_guard lock(st.mutex); - std::uint64_t total = 0; - for (const auto& [key, slot] : st.memory) { - total += slot.data.size() * ((slot.option & SD_MEMORY_DOUBLE_BUFFER) != 0 ? 2u : 1u); - } - return total; -} - -} // namespace -} // namespace savedata - -#endif diff --git a/core/libs/prx/libSceSaveDataDialog.native/CMakeLists.txt b/core/libs/prx/libSceSaveDataDialog.native/CMakeLists.txt index ddbc456db5..6c5c408051 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveDataDialog.native/CMakeLists.txt @@ -1,6 +1,5 @@ -add_library(libSceSaveDataDialog.native SHARED EXCLUDE_FROM_ALL Export.cpp) -target_include_directories(libSceSaveDataDialog.native PRIVATE ${LIBS_INCLUDE_DIR}) -configure_windows_unwind(libSceSaveDataDialog.native) +include(${CMAKE_CURRENT_SOURCE_DIR}/SaveDataDialogLibrary.cmake) +add_sce_savedata_dialog_library(libSceSaveDataDialog.native) if(BUILD_TESTING) add_test_executable(guest_savedata_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataDialog.cpp) diff --git a/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialogLibrary.cmake b/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialogLibrary.cmake new file mode 100644 index 0000000000..f4f82ce925 --- /dev/null +++ b/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialogLibrary.cmake @@ -0,0 +1,5 @@ +function(add_sce_savedata_dialog_library target) + add_library(${target} SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp) + target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceSaveDataDialog/CMakeLists.txt b/core/libs/prx/libSceSaveDataDialog/CMakeLists.txt new file mode 100644 index 0000000000..1b23415ac8 --- /dev/null +++ b/core/libs/prx/libSceSaveDataDialog/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceSaveDataDialog.native/SaveDataDialogLibrary.cmake) +add_sce_savedata_dialog_library(libSceSaveDataDialog) diff --git a/core/libs/prx/libSceSaveDataDialog/Export.cpp b/core/libs/prx/libSceSaveDataDialog/Export.cpp deleted file mode 100644 index 068c96f145..0000000000 --- a/core/libs/prx/libSceSaveDataDialog/Export.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libSceSaveDataDialog.native/SaveDataDialog.hpp" -#include "prx/libc/include/General.hpp" - - -namespace { -// Throwing from these reaches std::terminate and aborts the game process. -int SoftFailNp(const char* func) { - std::fprintf(stderr, "[SOFT-NP] %s -> 0 (unimplemented SCE API - no-op)\n", func); - std::fflush(stderr); - return 0; -} -} // namespace - -extern "C" { - - int APS5_VABI sceSaveDataDialogClose(const void* close_param) { - (void)close_param; - return SoftFailNp(__func__); - } - - int APS5_VABI sceSaveDataDialogGetResult(void* result) { - // Never return 0 with an untouched out-struct: the game would assert on the garbage later. - if (result == nullptr) { - return SAVE_DATA_DIALOG_ERROR_ARG_NULL; - } - auto* r = static_cast(result); - std::memset(r, 0, sizeof(*r)); - r->result = SAVE_DATA_DIALOG_RESULT_OK; - r->button_id = SAVE_DATA_DIALOG_BUTTON_ID_OK; - return SoftFailNp(__func__); - } - - int APS5_VABI sceSaveDataDialogInitialize(void) { - return SoftFailNp(__func__); - } - - int APS5_VABI sceSaveDataDialogIsReadyToDisplay(void) { - return SoftFailNp(__func__); - } - - int APS5_VABI sceSaveDataDialogOpen(const void* param) { - (void)param; - return SoftFailNp(__func__); - } - - int APS5_VABI sceSaveDataDialogProgressBarInc(int target, uint32_t delta) { - (void)target; - (void)delta; - return SoftFailNp(__func__); - } - - int APS5_VABI sceSaveDataDialogProgressBarSetValue(int target, uint32_t rate) { - (void)target; - (void)rate; - return SoftFailNp(__func__); - } - - int APS5_VABI sceSaveDataDialogTerminate(void) { - return SoftFailNp(__func__); - } - -} diff --git a/core/libs/tests/SaveDataWriteReplacement.cpp b/core/libs/tests/SaveDataWriteReplacement.cpp index e874060c9f..aa76fcdd9f 100644 --- a/core/libs/tests/SaveDataWriteReplacement.cpp +++ b/core/libs/tests/SaveDataWriteReplacement.cpp @@ -16,9 +16,6 @@ extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); int APS5_VABI sceSaveDataTerminate(void); int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2*, SaveDataMemorySetupResult*); -#ifndef SAVEDATA_NATIVE_BACKEND -int APS5_VABI sceSaveDataSyncSaveDataMemory(const void*); -#endif } static void Check(bool value, int line) { @@ -38,12 +35,7 @@ int main() { std::filesystem::current_path(root); constexpr std::int32_t userId = 7531; -#ifdef SAVEDATA_NATIVE_BACKEND const auto savePath = std::filesystem::path("_sd_mem") / ("u" + std::to_string(userId)) / "slot0.bin"; -#else - const auto savePath = std::filesystem::path("_sd") / "sce_sdmemory" / - std::to_string(userId) / "memory.dat"; -#endif std::filesystem::create_directories(savePath.parent_path()); const std::vector oldData{'o', 'l', 'd'}; { @@ -58,18 +50,7 @@ int main() { setup.user_id = userId; setup.memory_size = oldData.size() + 3; SaveDataMemorySetupResult result{}; - int status = sceSaveDataSetupSaveDataMemory2(&setup, &result); - -#ifndef SAVEDATA_NATIVE_BACKEND - struct MemorySyncParam { - std::int32_t user_id; - std::uint32_t slot_id; - std::uint32_t option; - } sync{userId, 0, 0}; - if (status == 0) { - status = sceSaveDataSyncSaveDataMemory(&sync); - } -#endif + const int status = sceSaveDataSetupSaveDataMemory2(&setup, &result); Require(result.existed_memory_size == oldData.size()); Require(status == 0); @@ -83,9 +64,7 @@ int main() { Require(std::all_of(savedData.begin() + static_cast(oldData.size()), savedData.end(), [](char byte) { return byte == 0; })); -#ifdef SAVEDATA_NATIVE_BACKEND Require(sceSaveDataTerminate() == 0); -#endif std::filesystem::current_path(originalDirectory); std::filesystem::remove_all(root); } diff --git a/docs/dev/debt/libSceSaveData.md b/docs/dev/debt/libSceSaveData.md new file mode 100644 index 0000000000..67e0b2ff8c --- /dev/null +++ b/docs/dev/debt/libSceSaveData.md @@ -0,0 +1,5 @@ +# libSceSaveData technical debt + +### Functional + +- [libSceSaveData](../../../core/libs/prx/libSceSaveData/CMakeLists.txt) is built from the libSceSaveData.native sources, so a title that imports save data from `libSceSaveData`, as PS4 titles do, gets the same functions. The separate implementation it had before kept save-data memory in `_sd/sce_sdmemory//memory.dat` and mount aliases in `_sd/_aliases`; neither is read, so memory saved through that library is not found From c1929ab9b676aea96e2fe6659bdda91c4363df03 Mon Sep 17 00:00:00 2001 From: Luis Gerardo Aguilar <100291529+LuisAguilarDev@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:30:13 -0500 Subject: [PATCH 003/702] fix(audioout2): keep each port's grain when ports share a buffer A title can fill one buffer and hand it to several ports in turn before advancing the context. Reading the buffer only at push time gave every port the contents set last, so the earlier ports played silence. Each set now copies the grain, and a grain whose buffer is handed to another port afterwards plays its copy. Buffers no other port is given are still read at push time. --- .../libSceAudioOut/src/AudioOut2Internal.hpp | 10 ++++- .../prx/libSceAudioOut/src/AudioOut2Port.cpp | 37 +++++++++++++++++-- .../tests/AudioOut2PortBuffers.cpp | 24 ++++++++++++ docs/dev/debt/libSceAudioOut.md | 2 +- 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index c58bd4c542..b09b4e444c 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -56,6 +56,12 @@ struct AudioOut2CapturedPort { using AudioOut2Grain = std::vector; +struct AudioOut2QueuedData { + const void* data = nullptr; + std::vector pcm; + bool reused = false; +}; + struct AudioOut2Port { bool used = false; std::uint64_t generation = 0; @@ -68,9 +74,11 @@ struct AudioOut2Port { bool int16 = false; const AudioOut2StereoFold* fold = nullptr; const void* data = nullptr; - std::deque queuedData; + std::deque queuedData; const void* lastBuffer = nullptr; const void* repeatedData = nullptr; + std::vector repeatedPcm; + bool repeatedReused = false; float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; std::uint64_t dataSets = 0; std::uint64_t attributeTraces = 0; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index 14c9bc2c63..bb8d062247 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -109,13 +109,20 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snaps auto& port = g_ports[index]; if (!port.used || port.context != &context || port.channels == 0) continue; const void* data = port.repeatedData; + const std::vector* setPcm = port.repeatedReused ? &port.repeatedPcm : nullptr; + AudioOut2QueuedData queued; if (!port.queuedData.empty()) { - data = port.queuedData.front(); + queued = std::move(port.queuedData.front()); port.queuedData.pop_front(); + data = queued.data; + setPcm = queued.reused ? &queued.pcm : nullptr; } if (data != nullptr) { AudioOut2CapturedPort captured{index, port.generation, data}; - if (snapshot) { + if (setPcm != nullptr) { + captured.pcm = *setPcm; + std::copy_n(port.volume, port.channels, captured.volume.begin()); + } else if (snapshot) { captured.pcm.resize(static_cast(context.grain) * port.channels * (port.int16 ? sizeof(std::int16_t) : sizeof(float))); std::memcpy(captured.pcm.data(), data, captured.pcm.size()); std::copy_n(port.volume, port.channels, captured.volume.begin()); @@ -159,6 +166,24 @@ void AudioOut2ReleasePorts(const AudioOut2Context& context) { } } +static std::vector CopyGrain(const AudioOut2Port& port) { + if (port.data == nullptr || port.context == nullptr) return {}; + std::vector pcm(static_cast(port.context->grain) * port.channels * (port.int16 ? sizeof(std::int16_t) : sizeof(float))); + std::memcpy(pcm.data(), port.data, pcm.size()); + return pcm; +} + +static void MarkBufferReused(const AudioOut2Port& setter) { + if (setter.data == nullptr) return; + for (auto& port : g_ports) { + if (!port.used || &port == &setter || port.context != setter.context) continue; + for (auto& queued : port.queuedData) { + if (queued.data == setter.data) queued.reused = true; + } + if (port.repeatedData == setter.data) port.repeatedReused = true; + } +} + static void TraceAttribute(AudioOut2PortHandle handle, const AudioOut2Port& port, const AudioOut2Attribute& attribute, const char* verdict) { if (!AudioOut2TraceEnabled()) return; char values[128] = ""; @@ -266,9 +291,13 @@ int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const Audi verdict = "null value"; } else if (attribute.attribute_id == ATTRIBUTE_DATA && attribute.value_size == sizeof(entry->data)) { std::memcpy(&entry->data, attribute.value, sizeof(entry->data)); - entry->queuedData.push_back(entry->data); - if (entry->queuedData.size() > QUEUED_DATA_MAX) entry->queuedData.pop_front(); + MarkBufferReused(*entry); + AudioOut2QueuedData queued{entry->data, CopyGrain(*entry)}; entry->repeatedData = entry->data != nullptr && (entry->lastBuffer == nullptr || entry->lastBuffer == entry->data) ? entry->data : nullptr; + entry->repeatedPcm = queued.pcm; + entry->repeatedReused = false; + entry->queuedData.push_back(std::move(queued)); + if (entry->queuedData.size() > QUEUED_DATA_MAX) entry->queuedData.pop_front(); if (entry->data != nullptr) entry->lastBuffer = entry->data; entry->dataSets++; verdict = "pcm data pointer"; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp index b1d5725eca..27d938c27c 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -286,6 +286,28 @@ void TestRenderingSizedByLevelStaysWithinQueueDepth() { RequirePlayed(played, order); } +void TestBufferReusedAcrossPortsKeepsEachPortsGrain(bool advanced) { + const auto voice = Grain(0.01f); + const std::vector silence(voice.size(), 0.0f); + auto shared = voice; + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + AudioOut2PortParam portParams{}; + portParams.port_type = portTypeMain; + portParams.data_format = formatStereoFloat; + portParams.sampling_freq = frequency; + AudioOut2PortHandle other = 0; + Require(sceAudioOut2PortCreate(context, &portParams, &other) == 0); + SetData(port, shared.data()); + shared = silence; + SetData(other, shared.data()); + if (advanced) Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + SetData(port, nullptr); + SetData(other, nullptr); + }, advanced); + RequirePlayed(played, {&voice}); +} + } int main() { @@ -299,5 +321,7 @@ int main() { TestAdvancingPortWithoutNewBufferPlaysSilence(); TestRewrittenBufferRepeats(); TestRenderingSizedByLevelStaysWithinQueueDepth(); + TestBufferReusedAcrossPortsKeepsEachPortsGrain(false); + TestBufferReusedAcrossPortsKeepsEachPortsGrain(true); return 0; } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 43372d1a15..7151588a5a 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -19,7 +19,7 @@ - [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 -- [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented +- [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Each set also copies the grain from the buffer: once another port of the context is given the same buffer, the grains already queued with it (and its repeat) play that copy instead of the buffer's later contents, as PPSA08587 fills one buffer and hands it to each of its three ports in turn before advancing. A buffer no other port is given is still read when the grain is pushed. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented - [Legacy vibration ports](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp): PPSA99902's [public input wrapper](https://github.com/Nazky/UnleashedRecomp-Prospero/blob/2ce067a1d51bb58670c7ee4a3aed893c504da805/UnleashedRecomp/hid/driver/sdl_hid.cpp#L316) opens type 10 with 256 stereo S16 or F32 frames at 48000 Hz. Mono duplicates its channel as AudioOut2 does; wider vibration layouts throw. Native controller output uses AudioOut2's four-channel layout, or its six-channel PulseAudio mapping. Without a controller sound card the existing RMS motor transform is applied to segments of at most 256 frames, scheduled by the legacy queue's frame count and rate; drain and close remove only that port's segments. Concurrent sources combine by the maximum per motor, and the pad consumer uses this approximation only for controllers other than DualSense. This host timing and motor model is not a console measurement or waveform reproduction; legacy native output is unmeasured on hardware, and user routing retains the single-controller limitation. AudioOut2 retains its separate 80 ms peak hold. - [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws From f24d9ce50c587bd6335496dabbd7d58b620d0f09 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 21 Oct 2026 04:00:36 +0000 Subject: [PATCH 004/702] perf(agc): pool large host buffers by size class BufferPool kept large host-visible buffers only for a request of the exact same size. Draw snapshots of guest buffers ask for a slightly different size almost every time, so the large tier sat at its budget, missing and evicting several buffers per frame, and each miss and eviction paid an allocation and an unmap in the driver. Large host requests now take the same classes as device-local ones, an eighth of their octave: the memory is allocated for the class and the VkBuffer and its mapping keep the requested size, and a retained buffer serves any later request in its class. A host request still takes only its own class, not the larger ones the device tier may use. The budget and eviction are unchanged. Capacity no longer depends on the memory properties, so it no longer takes them. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/BufferPool.hpp | 2 +- .../Graphics/src/BufferPool.cpp | 5 ++-- .../Graphics/src/Resources.cpp | 4 +-- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 26 ++++++++++++------- .../tests/execution/PooledBufferRange.cpp | 10 +++---- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 657c5351c4..cc20447721 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -63,7 +63,7 @@ public: BufferPool(const BufferPool&) = delete; BufferPool& operator=(const BufferPool&) = delete; // The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large. - static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); + static std::size_t Capacity(std::size_t bytes); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); static VkDeviceSize ImageBudget(VkDeviceSize deviceBudget); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 128970a898..5669e48eec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -366,11 +366,10 @@ bool BufferPool::DeviceTiered(VkMemoryPropertyFlags properties) { return (properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0 && DeviceTierEnabled(); } -std::size_t BufferPool::Capacity(std::size_t bytes, VkMemoryPropertyFlags properties) { +std::size_t BufferPool::Capacity(std::size_t bytes) { constexpr std::size_t smallest = 256; if (exactSizes()) return bytes; if (bytes < classLimit) return std::max(smallest, std::bit_ceil(bytes)); - if (!DeviceTiered(properties)) return bytes; const auto step = std::bit_floor(bytes) >> deviceClassBits; return (bytes + step - 1) / step * step; } @@ -390,7 +389,7 @@ BufferPool::Tier& BufferPool::tierFor(std::size_t capacity, VkMemoryPropertyFlag std::optional BufferPool::Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - const auto capacity = Capacity(bytes, properties); + const auto capacity = Capacity(bytes); std::lock_guard lock(mutex); ++clock; if (profile) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index 4f30207def..fdc40debd9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -37,7 +37,7 @@ VkBuffer BindExactBuffer(const Context& context, std::size_t size, VkBufferUsage } -Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) : context(context), size(size), capacity(BufferPool::Capacity(size, properties)), usage(BufferPool::Usage(usage, properties)), properties(properties) { +Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) : context(context), size(size), capacity(BufferPool::Capacity(size)), usage(BufferPool::Usage(usage, properties)), properties(properties) { Require(size != 0, "zero-sized GPU buffer"); const bool addressable = (usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0; Require(!addressable || context.bufferDeviceAddress, "buffer device address is not enabled"); @@ -143,7 +143,7 @@ void Buffer::Invalidate() { Check(context.Function("vkInvalidateMappedMemoryRanges")(context.device, 1, &range), "vkInvalidateMappedMemoryRanges"); } -DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsageFlags usage) : context(context), size(size), capacity(BufferPool::Capacity(size, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)), usage(BufferPool::Usage(usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { +DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsageFlags usage) : context(context), size(size), capacity(BufferPool::Capacity(size)), usage(BufferPool::Usage(usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { Require(size != 0, "zero-sized device buffer"); cache = GetBufferPool(context); if (const auto allocation = cache->Take(size, this->usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index abdf8f0166..e562a15262 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -387,7 +387,7 @@ void Budget() { Expect(MemoryOf(probe.Handle()) != first, "the least recently used device buffer survived the eviction"); } -void AddressAndHostUnchanged() { +void AddressAndHostClasses() { mock = MockDevice{}; auto context = mockContext(); VkDeviceMemory scratch = VK_NULL_HANDLE; @@ -399,17 +399,23 @@ void AddressAndHostUnchanged() { Expect(MemoryOf(addressed.Handle()) != scratch && addressed.DeviceAddress() != 0, "an addressable device-local buffer took a scratch buffer without a device address"); Buffer shadow(context, 2 * MiB - 256, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); Expect(MemoryOf(shadow.Handle()) == scratch, "a device-local staging buffer did not share the scratch buffers' class"); - VkBuffer host = VK_NULL_HANDLE; + VkDeviceMemory host = VK_NULL_HANDLE; { Buffer copy(context, 10 * MiB + 4096, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible); - host = copy.Handle(); - Expect(mock.sizes.at(host) == 10 * MiB + 4096, "a large host buffer was rounded to a class"); - Expect(mock.usages.at(host) == VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "a host buffer was created with more usages than asked"); + host = MemoryOf(copy.Handle()); + Expect(mock.sizes.at(copy.Handle()) == 10 * MiB + 4096, "a 10 MiB + 4 KiB host buffer was created with " + std::to_string(mock.sizes.at(copy.Handle())) + " bytes, not the requested size"); + Expect(ClassOf(copy.Handle()) == 11 * MiB, "a 10 MiB + 4 KiB host buffer has " + std::to_string(ClassOf(copy.Handle())) + " bytes of memory, not its 11 MiB class"); + Expect(mock.usages.at(copy.Handle()) == VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "a host buffer was created with more usages than asked"); + Expect(copy.Bytes().size() == 10 * MiB + 4096, "a host buffer maps its class instead of the requested size"); } - Buffer other(context, 10 * MiB + 8192, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible); - Expect(other.Handle() != host, "a large host buffer of another exact size was reused"); - Buffer same(context, 10 * MiB + 4096, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible); - Expect(same.Handle() == host, "a large host buffer of the same exact size was not reused"); + const auto made = mock.allocations; + { + Buffer other(context, 10 * MiB + 8192, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible); + Expect(MemoryOf(other.Handle()) == host && mock.allocations == made, "a retained 11 MiB host buffer did not serve another size in its class"); + Expect(mock.sizes.at(other.Handle()) == 10 * MiB + 8192 && other.Bytes().size() == 10 * MiB + 8192, "a reused host buffer kept the size of the earlier one"); + } + Buffer larger(context, 11 * MiB + 4096, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible); + Expect(MemoryOf(larger.Handle()) != host && mock.allocations == made + 1, "a host buffer was served by a retained buffer of a smaller class"); } void SmallDeviceClasses() { @@ -1022,7 +1028,7 @@ int main(int argc, char** argv) { KeptUntilFence(); SmallDeviceRetention(); Budget(); - AddressAndHostUnchanged(); + AddressAndHostClasses(); SmallDeviceClasses(); SlabMetadataFailure(); SlabMapFailure(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index 785886aa84..8d6b79ba1b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -485,7 +485,7 @@ std::uint32_t ReadPastEnd(const Context& context, AgcDriver::Graphics::Buffer& s void RobustReadPastEnd(const Context& context) { constexpr VkBufferUsageFlags usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT; - const auto classBytes = BufferPool::Capacity(RequestedBytes, HostMemory); + const auto classBytes = BufferPool::Capacity(RequestedBytes); Require(classBytes > (PastEnd + 1) * sizeof(std::uint32_t), "the requested buffer's size class does not reach the word read past its end"); { AgcDriver::Graphics::Buffer earlier(context, classBytes, usage); @@ -515,10 +515,10 @@ void ExpectCreated(std::initializer_list sizes, const std::string& void ExactSizes(const Context& context) { constexpr VkBufferUsageFlags usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; - const auto hostClass = BufferPool::Capacity(100, HostMemory); - const auto deviceClass = BufferPool::Capacity(3000, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Require(BufferPool::Capacity(80, HostMemory) == hostClass && hostClass != 80, "80 and 100 bytes are not one host size class"); - Require(BufferPool::Capacity(2500, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) == deviceClass && deviceClass != 2500, "2500 and 3000 bytes are not one device size class"); + const auto hostClass = BufferPool::Capacity(100); + const auto deviceClass = BufferPool::Capacity(3000); + Require(BufferPool::Capacity(80) == hostClass && hostClass != 80, "80 and 100 bytes are not one host size class"); + Require(BufferPool::Capacity(2500) == deviceClass && deviceClass != 2500, "2500 and 3000 bytes are not one device size class"); CreatedSizes().clear(); { AgcDriver::Graphics::Buffer fresh(context, 100, usage); From ad5516e2db2c76bc806cfd1fec407429ed26ae8b Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Wed, 21 Oct 2026 13:08:58 +0300 Subject: [PATCH 005/702] docs(libSceSaveData): move the pending declarations entry to the native debt file The declarations now live in libSceSaveData.native/Export.cpp, which both save data libraries are built from, so their entry belongs in that library's debt file. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceSaveData.md | 4 ---- docs/dev/debt/libSceSaveData.native.md | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/dev/debt/libSceSaveData.md b/docs/dev/debt/libSceSaveData.md index 2bc792bf68..67e0b2ff8c 100644 --- a/docs/dev/debt/libSceSaveData.md +++ b/docs/dev/debt/libSceSaveData.md @@ -1,9 +1,5 @@ # libSceSaveData technical debt -### Unknown function info - -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceSaveDataAbort`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccountForSystemBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataLight`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataBroken`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersion`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersionLatest`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataClearProgress`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCreateUploadData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDebugGetThreadId`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteAllUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteProspero`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteTitle`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDirNameSearchInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAllSize`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAppLaunchedUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadConditions`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadRequestInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetBoundPsnAccountCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetClientThreadPriority`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetCloudQuotaInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetMountedSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetProgress`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootDir`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootUsbPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSavePoint`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetUpdatedDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitialize2`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitializeForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsDeletingUsbDb`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsMounted`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDelete`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMount5`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRebuildDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRegisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreLoadSaveDataMemory`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetSaveDataLibraryUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataShutdownStart`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSupportedFakeBrokenStatus`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSyncCloudList`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountWithBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUnregisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp). - ### Functional - [libSceSaveData](../../../core/libs/prx/libSceSaveData/CMakeLists.txt) is built from the libSceSaveData.native sources, so a title that imports save data from `libSceSaveData`, as PS4 titles do, gets the same functions. The separate implementation it had before kept save-data memory in `_sd/sce_sdmemory//memory.dat` and mount aliases in `_sd/_aliases`; neither is read, so memory saved through that library is not found diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index db7aeb7660..8ab46bfae9 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -12,6 +12,7 @@ - [sceSaveDataDirNameSearch, sceSaveDataGetMountInfo, sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the 64 KiB block of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/c6d304cc3328bad62cb671327ace181110cf2ddb/src/SharpEmu.Libs/SaveData/SaveDataExports.cs#L82-L84) and the 48 to 16384-block range of SharpEmu and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L30-L31) are not measured on a console, and file system overhead is not counted. What the console does with a `blocks` outside that range is unknown: KytyPS5 rejects the creating mount and SharpEmu clamps it; here a larger one is kept as 16384 and a smaller non-zero one as given. A save's size is the `blocks` of the mount that created it, kept in `.blocks`, else that of its next mount that names one, else 16384; its free blocks are that size less its files, each rounded up to 64 KiB blocks - [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 - [sceSaveDataCheckIpmiIfSize, sceSaveDataDebug, sceSaveDataDebugCheckBackupData, sceSaveDataDebugCleanMount, sceSaveDataDebugCompiledSdkVersion, sceSaveDataDebugCreateSaveDataRoot, sceSaveDataDebugProspero, sceSaveDataDebugRemoveSaveDataRoot and sceSaveDataDebugTarget](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSaveData.native.c). The seven no-argument prototypes follow the PS4 stubs in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/save_data/savedata.cpp#L653-L725), not measured on a console; no credible prototypes were found for the other two. All declarations have no parameters solely to resolve known imports and make no ABI claim. +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceSaveDataAbort`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccountForSystemBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataLight`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataBroken`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersion`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersionLatest`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataClearProgress`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCreateUploadData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDebugGetThreadId`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteAllUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteProspero`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteTitle`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDirNameSearchInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAllSize`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAppLaunchedUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadConditions`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadRequestInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetBoundPsnAccountCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetClientThreadPriority`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetCloudQuotaInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetMountedSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetProgress`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootDir`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootUsbPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSavePoint`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetUpdatedDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitialize2`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitializeForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsDeletingUsbDb`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsMounted`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDelete`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMount5`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRebuildDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRegisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreLoadSaveDataMemory`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetSaveDataLibraryUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataShutdownStart`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSupportedFakeBrokenStatus`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSyncCloudList`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountWithBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUnregisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp). ### Functional From fab834c6924bdcacd854a4e6461f28c7e2d40a26 Mon Sep 17 00:00:00 2001 From: Nicolas Gonzalez Date: Wed, 21 Oct 2026 03:29:51 -0300 Subject: [PATCH 006/702] fix(libSceNet): refuse IPv4 components with a leading zero on macOS FreeBSD's inet_pton refuses an IPv4 address with a component that has a leading zero, such as "01.2.3.4", and guest_sce_net expects sceNetInetPton to refuse it. macOS's inet_pton accepts it as 1.2.3.4, so sceNetInetPton refuses it there before calling the host. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index d6a9bd0b15..7d182ae4f5 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1760,6 +1760,13 @@ int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { return fail(NET_EAFNOSUPPORT); } const std::string text = af == NET_AF_INET ? std::string(src, std::strcspn(src, " \t\n\v\f\r")) : std::string(src); +#ifdef __APPLE__ + if (af == NET_AF_INET) { + for (std::size_t i = 0; i + 1 < text.size(); ++i) { + if (text[i] == '0' && (i == 0 || text[i - 1] == '.') && text[i + 1] >= '0' && text[i + 1] <= '9') return 0; + } + } +#endif // __APPLE__ std::array address{}; #ifdef _WIN32 const int converted = InetPtonA(af == NET_AF_INET ? AF_INET : AF_INET6, text.c_str(), address.data()); From 14052658e5b19f2ab1e875df390c26db46ee50c9 Mon Sep 17 00:00:00 2001 From: Nicolas Gonzalez Date: Wed, 21 Oct 2026 03:30:00 -0300 Subject: [PATCH 007/702] fix(libSceNet): report EPROTONOSUPPORT for a protocol of another socket type on macOS FreeBSD's socket fails with EPROTONOSUPPORT when the protocol belongs to another socket type, such as IPPROTO_UDP for SOCK_STREAM, and guest_sce_net expects that error from sceNetSocket. XNU fails with EPROTOTYPE instead. sceNetSocket only passes the stream, datagram and raw types, which XNU supports with protocol 0, so a host EPROTOTYPE there is always a protocol of another type. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 7d182ae4f5..7d0b926205 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -797,7 +797,12 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) } if (!initialize_sockets()) return fail(5); const NativeSocket native = ::socket(family == NET_AF_INET ? AF_INET : AF_INET6, type, protocol); - if (native == INVALID_NATIVE_SOCKET) return fail(native_error()); + if (native == INVALID_NATIVE_SOCKET) { +#ifdef __APPLE__ + if (errno == EPROTOTYPE) return fail(NET_EPROTONOSUPPORT); +#endif // __APPLE__ + return fail(native_error()); + } #ifdef _WIN32 u_long nonblocking = 1; if (ioctlsocket(native, FIONBIO, &nonblocking) != 0) { From ac9fd36cf0351879f57c889fefcdf11348be190f Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 16:13:11 +0000 Subject: [PATCH 008/702] fix(libc): start sceLibcBacktraceSelf's frames at its caller CaptureFrames dropped the first two captured addresses, taking them to be its own frame and the export's. A frame the unwinder adds in between shifts that: in an ANYPS5_ENABLE_ASAN build the sanitizer's backtrace interceptor is one, so the first record was the export in libc.prx and guest_backtrace failed with "first frame is not in the calling module". The exports now pass their own return address, and the records start at the captured address equal to it. When no captured address matches, the first two are dropped as before. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Backtrace.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index a07a476b4a..a74a164470 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -55,7 +55,7 @@ struct FrameRecord { }; static_assert(sizeof(FrameRecord) == NameOffset); -[[gnu::noinline]] std::vector CaptureFrames(int limit) { +[[gnu::noinline]] std::vector CaptureFrames(int limit, const void* caller) { void* addresses[MaxFrames]; #if defined(__linux__) const int captured = backtrace(addresses, MaxFrames); @@ -76,8 +76,14 @@ static_assert(sizeof(FrameRecord) == NameOffset); _Unwind_Backtrace_nid_postfix(traceCallback, &state); const int captured = state.count; #endif + int first = 2; + for (int index = 0; index < captured; ++index) { + if (addresses[index] != caller) continue; + first = index; + break; + } std::vector frames; - for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { + for (int index = first; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { #if defined(__linux__) Dl_info info{}; if (dladdr(addresses[index], &info) == 0 || info.dli_fbase == nullptr || info.dli_fname == nullptr) continue; @@ -113,7 +119,7 @@ extern "C" { int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int limit, int* count, std::size_t* size) { if (count == nullptr || size == nullptr) throw std::invalid_argument("sceLibcBacktraceGetBufferSize: null output"); - const auto frames = CaptureFrames(limit); + const auto frames = CaptureFrames(limit, __builtin_return_address(0)); std::size_t total = 0; for (const auto& frame : frames) total += RecordSize(frame); *count = static_cast(frames.size()); @@ -123,7 +129,7 @@ int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int limit, int* count, s int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int limit, void* buffer, std::size_t size, int* count) { if (buffer == nullptr || count == nullptr) throw std::invalid_argument("sceLibcBacktraceSelf: null argument"); - const auto frames = CaptureFrames(limit); + const auto frames = CaptureFrames(limit, __builtin_return_address(0)); auto* cursor = static_cast(buffer); FrameRecord* previous = nullptr; int written = 0; From 59d661a23161332acde324f4a2b00cb611e76838 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 21 Oct 2026 17:07:48 -0300 Subject: [PATCH 009/702] feat(libSceFreeType): implement advance queries --- core/libs/prx/libSceFreeType/CMakeLists.txt | 14 + core/libs/prx/libSceFreeType/src/Advances.cpp | 74 +++++ core/libs/tests/GuestFreeTypeAdvances.cpp | 306 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 1 + 4 files changed, 395 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/src/Advances.cpp create mode 100644 core/libs/tests/GuestFreeTypeAdvances.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index e0a3b3e116..2ef8c8ab4b 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -3,6 +3,20 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL src/GuestObjects.cpp ) target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) +target_sources(libSceFreeType PRIVATE src/Advances.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_advances_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeAdvances.cpp) + target_include_directories(guest_freetype_advances_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_advances_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_advances_tests) + add_test(NAME guest_freetype_advances COMMAND guest_freetype_advances_tests) + if(WIN32) + set_tests_properties(guest_freetype_advances PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_link_libraries(libSceFreeType PRIVATE freetype) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libSceFreeType PRIVATE LINKER:--exclude-libs,ALL) diff --git a/core/libs/prx/libSceFreeType/src/Advances.cpp b/core/libs/prx/libSceFreeType/src/Advances.cpp new file mode 100644 index 0000000000..17e410c65d --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Advances.cpp @@ -0,0 +1,74 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/General.hpp" + +#include FT_ADVANCES_H +#include FT_FONT_FORMATS_H +#include FT_TRUETYPE_TABLES_H + +#include +#include +#include + +using namespace FreeType; + +namespace { + +bool FastQueryLoadsGlyph(FT_Face face, std::int32_t flags) { + const char* format = FT_Get_Font_Format(face); + if (!format || std::strcmp(format, "CFF") != 0) return false; + if (!FT_IS_SFNT(face)) return true; + if (flags & FT_LOAD_VERTICAL_LAYOUT) return !FT_HAS_VERTICAL(face); + const auto* header = static_cast(FT_Get_Sfnt_Table(face, ft_sfnt_hhea)); + return !header || header->number_Of_HMetrics == 0; +} + +int GetAdvances(GuestFaceRec* face, std::uint32_t start, std::uint32_t count, std::int32_t flags, std::int64_t* advances, bool single) { + GuestFace* owner = FaceOf(face); + if (!owner) return FT_Err_Invalid_Face_Handle; + if (!advances) return FT_Err_Invalid_Argument; + const auto numGlyphs = static_cast(owner->host->num_glyphs); + const std::uint32_t end = start + count; + if (start >= numGlyphs || end < start || end > numGlyphs) return FT_Err_Invalid_Glyph_Index; + if (count == 0) return 0; + + std::vector values(count); + if constexpr (sizeof(FT_Fixed) == sizeof(std::int64_t)) { + std::memcpy(values.data(), advances, values.size() * sizeof(FT_Fixed)); + } + const auto query = [&](std::int32_t loadFlags) { + return single ? FT_Get_Advance(owner->host, start, loadFlags, values.data()) + : FT_Get_Advances(owner->host, start, count, loadFlags, values.data()); + }; + FT_Error error = query(flags | FT_ADVANCE_FLAG_FAST_ONLY); + bool syncSlot = error != FT_Err_Unimplemented_Feature && FastQueryLoadsGlyph(owner->host, flags); + if (error == FT_Err_Unimplemented_Feature && !(flags & FT_ADVANCE_FLAG_FAST_ONLY)) { + error = query(flags); + syncSlot = true; + } + if (syncSlot) SyncSlot(*owner); + if constexpr (sizeof(FT_Fixed) == sizeof(std::int64_t)) { + std::memcpy(advances, values.data(), values.size() * sizeof(FT_Fixed)); + } else { + if (error != 0 && error != FT_Err_Unimplemented_Feature) { + NotImplemented_nid_no_patch("FreeType advance query: partial output after a native backend error with 32-bit FT_Fixed"); + } + if (error == 0) { + for (std::uint32_t index = 0; index < count; ++index) advances[index] = values[index]; + } + } + return error; +} + +} + +extern "C" { + +int APS5_VABI FT_Get_Advance_nid_postfix(GuestFaceRec* face, std::uint32_t glyphIndex, std::int32_t flags, std::int64_t* advance) { + return GetAdvances(face, glyphIndex, 1, flags, advance, true); +} + +int APS5_VABI FT_Get_Advances_nid_postfix(GuestFaceRec* face, std::uint32_t start, std::uint32_t count, std::int32_t flags, std::int64_t* advances) { + return GetAdvances(face, start, count, flags, advances, false); +} + +} diff --git a/core/libs/tests/GuestFreeTypeAdvances.cpp b/core/libs/tests/GuestFreeTypeAdvances.cpp new file mode 100644 index 0000000000..da91613b2e --- /dev/null +++ b/core/libs/tests/GuestFreeTypeAdvances.cpp @@ -0,0 +1,306 @@ +#include "FreeTypeFontFixture.hpp" +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +#include +#include FT_FREETYPE_H +#include FT_ADVANCES_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Init_FreeType_nid_postfix(void**); +int APS5_VABI FT_Done_FreeType_nid_postfix(void*); +int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); +int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t); +int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); +int APS5_VABI FT_Get_Advance_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t, std::int64_t*); +int APS5_VABI FT_Get_Advances_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t, std::int32_t, std::int64_t*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType advance check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr std::int64_t Sentinel = 0x1122334455667788; + +struct Fixture { + std::vector bytes; + FT_Library library = nullptr; + FT_Face native = nullptr; + void* guestLibrary = nullptr; + GuestFaceRec* guest = nullptr; + + explicit Fixture(std::vector font) : bytes(std::move(font)) { + Require(FT_Init_FreeType(&library) == 0); + Require(FT_New_Memory_Face(library, bytes.data(), static_cast(bytes.size()), 0, &native) == 0); + Require(FT_Init_FreeType_nid_postfix(&guestLibrary) == 0); + Require(FT_New_Memory_Face_nid_postfix(guestLibrary, bytes.data(), static_cast(bytes.size()), 0, &guest) == 0); + SetSize(37, 53); + } + + ~Fixture() { + Require(FT_Done_FreeType_nid_postfix(guestLibrary) == 0); + Require(FT_Done_FreeType(library) == 0); + } + + void SetSize(unsigned width, unsigned height) { + Require(FT_Set_Pixel_Sizes(native, width, height) == 0); + Require(FT_Set_Pixel_Sizes_nid_postfix(guest, width, height) == 0); + } + + void Load(unsigned glyph) { + Require(FT_Load_Glyph(native, glyph, FT_LOAD_DEFAULT) == 0); + Require(FT_Load_Glyph_nid_postfix(guest, glyph, FT_LOAD_DEFAULT) == 0); + } + + void CheckSlot() const { + const auto& actual = *guest->glyph; + const auto& expected = *native->glyph; + Require(actual.glyph_index == expected.glyph_index && actual.format == static_cast(expected.format)); + Require(actual.metrics.width == expected.metrics.width && actual.metrics.height == expected.metrics.height); + Require(actual.metrics.horiBearingX == expected.metrics.horiBearingX && actual.metrics.horiBearingY == expected.metrics.horiBearingY); + Require(actual.metrics.horiAdvance == expected.metrics.horiAdvance && actual.metrics.vertAdvance == expected.metrics.vertAdvance); + Require(actual.advance.x == expected.advance.x && actual.advance.y == expected.advance.y); + Require(actual.linearHoriAdvance == expected.linearHoriAdvance && actual.linearVertAdvance == expected.linearVertAdvance); + Require(actual.outline.n_points == expected.outline.n_points && actual.outline.n_contours == expected.outline.n_contours); + Require(actual.outline.flags == expected.outline.flags); + for (unsigned index = 0; index < actual.outline.n_points; ++index) { + Require(actual.outline.points[index].x == expected.outline.points[index].x); + Require(actual.outline.points[index].y == expected.outline.points[index].y); + } + } +}; + +static void CheckSingle(Fixture& fixture, unsigned glyph, std::int32_t flags) { + fixture.Load(1); + FT_Fixed expected = static_cast(Sentinel); + const int error = FT_Get_Advance(fixture.native, glyph, flags, &expected); + std::array actual{Sentinel, Sentinel, Sentinel}; + Require(FT_Get_Advance_nid_postfix(fixture.guest, glyph, flags, &actual[1]) == error); + Require(actual.front() == Sentinel && actual.back() == Sentinel); + if (error == 0 || sizeof(FT_Fixed) == sizeof(std::int64_t)) Require(actual[1] == expected); + else Require(actual[1] == Sentinel); + fixture.CheckSlot(); +} + +static void CheckBatch(Fixture& fixture, unsigned start, unsigned count, std::int32_t flags) { + fixture.Load(1); + std::array expected; + expected.fill(static_cast(Sentinel)); + const int error = FT_Get_Advances(fixture.native, start, count, flags, expected.data()); + std::array actual; + actual.fill(Sentinel); + Require(FT_Get_Advances_nid_postfix(fixture.guest, start, count, flags, &actual[1]) == error); + Require(actual.front() == Sentinel && actual.back() == Sentinel); + for (unsigned index = 0; index < expected.size(); ++index) { + if ((error == 0 && index < count) || sizeof(FT_Fixed) == sizeof(std::int64_t)) Require(actual[index + 1] == expected[index]); + else Require(actual[index + 1] == Sentinel); + } + fixture.CheckSlot(); +} + +static void CheckFastPreservesGuestEdits(Fixture& fixture) { + fixture.Load(1); + auto* slot = fixture.guest->glyph; + Require(slot->outline.n_points > 0); + slot->outline.points[0].x += 137; + slot->outline.flags ^= 2; + const auto point = slot->outline.points[0].x; + const auto outlineFlags = slot->outline.flags; + const auto slotBefore = *slot; + std::array actual{Sentinel, Sentinel}; + Require(FT_Get_Advances_nid_postfix(fixture.guest, 0, 2, FT_LOAD_NO_SCALE | FT_ADVANCE_FLAG_FAST_ONLY, actual.data()) == 0); + Require(std::memcmp(&slotBefore, slot, sizeof(slotBefore)) == 0); + Require(slot->outline.points[0].x == point && slot->outline.flags == outlineFlags); + Require(FT_Get_Advance_nid_postfix(fixture.guest, 0, FT_LOAD_DEFAULT | FT_ADVANCE_FLAG_FAST_ONLY, actual.data()) == FT_Err_Unimplemented_Feature); + Require(slot->outline.points[0].x == point && slot->outline.flags == outlineFlags); + Require(FT_Get_Advance_nid_postfix(fixture.guest, 0, FT_LOAD_DEFAULT, actual.data()) == 0); + FT_Fixed expected = 0; + Require(FT_Get_Advance(fixture.native, 0, FT_LOAD_DEFAULT, &expected) == 0); + fixture.CheckSlot(); +} + +static void CheckAliasedOutput(Fixture& fixture) { + fixture.Load(1); + Require(FT_Get_Advance(fixture.native, 0, FT_LOAD_DEFAULT, &fixture.native->glyph->advance.x) == 0); + Require(FT_Get_Advance_nid_postfix(fixture.guest, 0, FT_LOAD_DEFAULT, &fixture.guest->glyph->advance.x) == 0); + fixture.CheckSlot(); +} + +static std::uint32_t Read32(const std::vector& bytes, std::size_t offset) { + return (std::uint32_t{bytes[offset]} << 24) | (std::uint32_t{bytes[offset + 1]} << 16) | + (std::uint32_t{bytes[offset + 2]} << 8) | bytes[offset + 3]; +} + +static std::vector BrokenGlyphFont() { + auto bytes = SquareGlyphFont('A', 913); + for (std::size_t offset = 12; offset + 16 <= bytes.size(); offset += 16) { + if (std::memcmp(bytes.data() + offset, "glyf", 4) != 0) continue; + const auto glyph = Read32(bytes, offset + 8); + bytes[glyph] = 0x7F; + bytes[glyph + 1] = 0xFF; + return bytes; + } + std::abort(); +} + +static void CffIndex(std::vector& font, const std::vector>& entries) { + Put16(font, static_cast(entries.size())); + if (entries.empty()) return; + font.push_back(1); + std::size_t offset = 1; + font.push_back(static_cast(offset)); + for (const auto& entry : entries) { + offset += entry.size(); + Require(offset <= 255); + font.push_back(static_cast(offset)); + } + for (const auto& entry : entries) font.insert(font.end(), entry.begin(), entry.end()); +} + +static std::vector CffFont() { + std::vector font{1, 0, 4, 1}; + CffIndex(font, {{'A', 'd', 'v', 'a', 'n', 'c', 'e'}}); + const auto topDict = font.size(); + CffIndex(font, {{0, 17}}); + CffIndex(font, {}); + CffIndex(font, {}); + Require(font.size() < 107); + font[topDict + 5] = static_cast(font.size() + 139); + CffIndex(font, {{249, 80, 14}, {254, 37, 239, 139, 21, 139, 249, 80, 248, 136, 139, 139, 253, 80, 5, 14}}); + return font; +} + +static std::vector OtfCffFont() { + const auto original = SquareGlyphFont('A', 913); + using Table = std::pair, std::vector>; + std::vector tables{{{'C', 'F', 'F', ' '}, CffFont()}}; + for (std::size_t record = 12; record < 12 + 7 * 16; record += 16) { + if (std::memcmp(original.data() + record, "glyf", 4) == 0 || std::memcmp(original.data() + record, "loca", 4) == 0) continue; + std::array tag; + std::memcpy(tag.data(), original.data() + record, tag.size()); + const auto start = Read32(original, record + 8); + const auto length = Read32(original, record + 12); + tables.push_back({tag, {original.begin() + start, original.begin() + start + length}}); + } + std::vector font; + Put32(font, 0x4F54544F); + PutAll16(font, {6, 64, 2, 32}); + std::size_t offset = 12 + tables.size() * 16; + for (const auto& [tag, data] : tables) { + font.insert(font.end(), tag.begin(), tag.end()); + Put32(font, 0); + Put32(font, static_cast(offset)); + Put32(font, static_cast(data.size())); + offset += (data.size() + 3) & ~std::size_t{3}; + } + for (const auto& [tag, data] : tables) { + font.insert(font.end(), data.begin(), data.end()); + font.resize((font.size() + 3) & ~std::size_t{3}); + } + return font; +} + +static void CheckCff() { + Fixture fixture(CffFont()); + for (const std::int32_t flags : std::array{FT_LOAD_NO_SCALE, FT_LOAD_NO_HINTING, + FT_ADVANCE_FLAG_FAST_ONLY | FT_LOAD_NO_SCALE, FT_LOAD_DEFAULT}) { + CheckSingle(fixture, 1, flags); + CheckBatch(fixture, 0, 2, flags); + } + fixture.Load(1); + Require(fixture.guest->glyph->outline.n_points > 0); + fixture.guest->glyph->outline.points[0].x += 137; + FT_Fixed expected = 0; + std::int64_t actual = Sentinel; + Require(FT_Get_Advance(fixture.native, 1, FT_LOAD_NO_SCALE, &expected) == 0 && expected < 0); + Require(FT_Get_Advance_nid_postfix(fixture.guest, 1, FT_LOAD_NO_SCALE, &actual) == 0 && actual == expected); + fixture.CheckSlot(); + + Fixture otf(OtfCffFont()); + for (const std::int32_t flags : std::array{FT_LOAD_NO_SCALE, FT_LOAD_NO_SCALE | FT_LOAD_VERTICAL_LAYOUT, FT_LOAD_DEFAULT}) { + CheckSingle(otf, 1, flags); + CheckBatch(otf, 0, 2, flags); + } + otf.Load(1); + Require(otf.guest->glyph->outline.n_points > 0); + otf.guest->glyph->outline.points[0].x += 137; + const auto point = otf.guest->glyph->outline.points[0].x; + Require(FT_Get_Advance_nid_postfix(otf.guest, 1, FT_LOAD_NO_SCALE, &actual) == 0); + Require(otf.guest->glyph->outline.points[0].x == point); + const auto flags = FT_LOAD_NO_SCALE | FT_LOAD_VERTICAL_LAYOUT; + Require(FT_Get_Advance(otf.native, 1, flags, &expected) == 0); + Require(FT_Get_Advance_nid_postfix(otf.guest, 1, flags, &actual) == 0 && actual == expected); + otf.CheckSlot(); +} + +static void CheckPartialFailure() { + Fixture fixture(BrokenGlyphFont()); + std::array expected{static_cast(Sentinel), static_cast(Sentinel)}; + const int error = FT_Get_Advances(fixture.native, 0, 2, FT_LOAD_DEFAULT, expected.data()); + Require(error != 0 && expected[0] != static_cast(Sentinel) && expected[1] == static_cast(Sentinel)); + std::array actual{Sentinel, Sentinel, Sentinel, Sentinel}; + if constexpr (sizeof(FT_Fixed) == sizeof(std::int64_t)) { + Require(FT_Get_Advances_nid_postfix(fixture.guest, 0, 2, FT_LOAD_DEFAULT, &actual[1]) == error); + Require(actual[1] == expected[0] && actual[2] == Sentinel); + } else { + bool threw = false; + try { + FT_Get_Advances_nid_postfix(fixture.guest, 0, 2, FT_LOAD_DEFAULT, &actual[1]); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && actual[1] == Sentinel && actual[2] == Sentinel); + } + Require(actual.front() == Sentinel && actual.back() == Sentinel); + fixture.CheckSlot(); +} + +int main() { + Fixture fixture(SquareGlyphFont('A', 913)); + for (const unsigned size : {7u, 37u, 113u}) { + fixture.SetSize(size, size + 11); + for (const std::int32_t flags : std::array{FT_LOAD_DEFAULT, FT_LOAD_NO_SCALE, FT_LOAD_NO_HINTING, FT_LOAD_VERTICAL_LAYOUT, + FT_LOAD_NO_SCALE | FT_LOAD_VERTICAL_LAYOUT, FT_LOAD_TARGET_LIGHT, FT_ADVANCE_FLAG_FAST_ONLY, + FT_ADVANCE_FLAG_FAST_ONLY | FT_LOAD_NO_SCALE, FT_ADVANCE_FLAG_FAST_ONLY | FT_LOAD_NO_HINTING}) { + CheckSingle(fixture, 0, flags); + CheckSingle(fixture, 1, flags); + CheckBatch(fixture, 0, 2, flags); + CheckBatch(fixture, 1, 1, flags); + } + } + for (const unsigned glyph : {2u, std::numeric_limits::max()}) CheckSingle(fixture, glyph, FT_LOAD_DEFAULT); + CheckBatch(fixture, 0, 0, FT_LOAD_DEFAULT); + CheckBatch(fixture, 1, 0, FT_LOAD_DEFAULT); + CheckBatch(fixture, 2, 0, FT_LOAD_DEFAULT); + CheckBatch(fixture, 1, 2, FT_LOAD_DEFAULT); + CheckBatch(fixture, 1, std::numeric_limits::max(), FT_LOAD_DEFAULT); + CheckBatch(fixture, std::numeric_limits::max(), 2, FT_LOAD_DEFAULT); + + std::int64_t output = Sentinel; + Require(FT_Get_Advance_nid_postfix(nullptr, 0, 0, &output) == FT_Err_Invalid_Face_Handle); + Require(FT_Get_Advance_nid_postfix(nullptr, 0, 0, nullptr) == FT_Err_Invalid_Face_Handle); + Require(FT_Get_Advance_nid_postfix(fixture.guest, 2, 0, nullptr) == FT_Err_Invalid_Argument); + Require(FT_Get_Advances_nid_postfix(nullptr, 0, 0, 0, &output) == FT_Err_Invalid_Face_Handle); + Require(FT_Get_Advances_nid_postfix(nullptr, 0, 0, 0, nullptr) == FT_Err_Invalid_Face_Handle); + Require(FT_Get_Advances_nid_postfix(fixture.guest, 2, 0, 0, nullptr) == FT_Err_Invalid_Argument); + Require(output == Sentinel); + CheckFastPreservesGuestEdits(fixture); + CheckAliasedOutput(fixture); + CheckPartialFailure(); + CheckCff(); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..35302f0bb1 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -5,6 +5,7 @@ - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions +- [FT_Get_Advance and FT_Get_Advances](../../../core/libs/prx/libSceFreeType/src/Advances.cpp) (libSceFreeType) - console signatures, errors and version-specific behavior are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftadvanc.c). On Windows, native `FT_Fixed` is 32 bits; successful outputs are widened to guest 64-bit slots, but backend errors other than `Unimplemented_Feature` throw because partially written results cannot be distinguished from untouched slots. Host overflow behavior remains that of the bundled FreeType build. - [FT_New_Memory_Face, FT_Done_Face and FT_Done_FreeType](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; they follow the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h) with LP64 guest arguments. Caller-owned font bytes must remain valid until the face is freed. From bc0f1dc27e78250d8c66700696e185b605433d80 Mon Sep 17 00:00:00 2001 From: Henryisdjfunkyho <197676650+Henryisdjfunkyho@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:14:22 -0700 Subject: [PATCH 010/702] fix(libkernel): wake timed sleeps for thread cancellation --- core/libs/prx/libkernel/CMakeLists.txt | 13 ++++++++++ core/libs/prx/libkernel/Time/Time.cpp | 14 ++++++++++- core/libs/tests/GuestSleepCancel.cpp | 35 +++++++++++++++++++++++--- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 635db8b212..0ef6107890 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1927,4 +1927,17 @@ if(BUILD_TESTING) ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endforeach() + foreach(sleepApi IN ITEMS usleep sce_usleep nanosleep _nanosleep sce_nanosleep sce_sleep) + foreach(sleepCase IN ITEMS blocked disabled normal pending pending_long) + set(sleepTest guest_sleep_cancel_${sleepApi}_${sleepCase}) + add_test(NAME ${sleepTest} COMMAND guest_sleep_cancel_tests ${sleepCase} ${sleepApi}) + add_test(NAME ${sleepTest}_coarse COMMAND guest_sleep_cancel_tests ${sleepCase} ${sleepApi}) + set_tests_properties(${sleepTest} ${sleepTest}_coarse PROPERTIES TIMEOUT 20) + set_tests_properties(${sleepTest}_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + if(WIN32) + set_tests_properties(${sleepTest} ${sleepTest}_coarse PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() endif() diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 411f92751d..cca1ab1476 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -276,7 +277,18 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint static void CancellableSleep(std::uint64_t nanos) { ThreadCancel::Check(); - SleepWake::Sleep(nanos); + if (nanos <= SleepWake::MAX_WAKEABLE_NANOS) { + SleepWake::Sleep(nanos); + } else { + const auto now = TimedWait::NowNanos(); + const auto deadline = now + std::min(nanos, std::numeric_limits::max() - now); + std::mutex mutex; + TimedWait::Condition condition; + std::unique_lock lock(mutex); + while (!ThreadCancel::Requested() && TimedWait::NowNanos() < deadline) { + ThreadCancel::Wait(condition, lock, deadline); + } + } ThreadCancel::Check(); } diff --git a/core/libs/tests/GuestSleepCancel.cpp b/core/libs/tests/GuestSleepCancel.cpp index 463a4a0cbe..3b3ea39e9f 100644 --- a/core/libs/tests/GuestSleepCancel.cpp +++ b/core/libs/tests/GuestSleepCancel.cpp @@ -1,5 +1,7 @@ #include "SceTypes.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Time/include/Time.hpp" #include #include #include @@ -16,6 +18,9 @@ int APS5_VABI scePthreadCancel(Pthread thread); int APS5_VABI scePthreadSetcancelstate(int state, int* oldState); void APS5_VABI scePthreadTestcancel(); unsigned int APS5_VABI sleep_nid_postfix(unsigned int seconds); +unsigned int APS5_VABI sceKernelSleep(unsigned int seconds); +int APS5_VABI usleep_nid_postfix(KernelUseconds microseconds); +int APS5_VABI _nanosleep_nid_postfix(const KernelTimespec* request, KernelTimespec* remaining); } static void Require(bool value, std::source_location location = std::source_location::current()) { @@ -32,15 +37,35 @@ static std::atomic returned{false}; static bool disabled = false; static bool pending = false; static unsigned seconds = LongSleep; +static std::string_view api = "sleep"; +static unsigned cleanupCalls = 0; static std::chrono::steady_clock::duration elapsed{}; +static void APS5_VABI Cleanup(void*) { + ++cleanupCalls; +} + +static int Sleep() { + if (api == "sleep") return static_cast(sleep_nid_postfix(seconds)); + if (api == "sce_sleep") return static_cast(sceKernelSleep(seconds)); + if (api == "usleep") return usleep_nid_postfix(seconds * 1000000U); + if (api == "sce_usleep") return sceKernelUsleep_nid_postfix(seconds * 1000000U); + const KernelTimespec request{seconds, 0}; + if (api == "nanosleep") return nanosleep_nid_postfix(&request, nullptr); + if (api == "_nanosleep") return _nanosleep_nid_postfix(&request, nullptr); + Require(api == "sce_nanosleep"); + return sceKernelNanosleep(&request, nullptr); +} + static void* APS5_VABI Sleeper(void*) { if (disabled) Require(scePthreadSetcancelstate(1, nullptr) == 0); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(Cleanup, nullptr, &cleanup); ready.store(true); ready.notify_all(); if (pending) release.wait(false); const auto start = std::chrono::steady_clock::now(); - Require(sleep_nid_postfix(seconds) == 0); + Require(Sleep() == 0); elapsed = std::chrono::steady_clock::now() - start; returned.store(true); if (disabled) { @@ -48,6 +73,7 @@ static void* APS5_VABI Sleeper(void*) { scePthreadTestcancel(); Require(false); } + __pthread_cleanup_pop_imp_nid_postfix(0); return nullptr; } @@ -73,11 +99,12 @@ static bool WaitForSleep(Pthread thread) { } int main(int count, char** arguments) { - Require(count == 2); + Require(count == 2 || count == 3); + if (count == 3) api = arguments[2]; const std::string_view mode(arguments[1]); disabled = mode == "disabled"; const bool zero = mode == "pending"; - pending = zero || disabled; + pending = zero || disabled || mode == "pending_long"; const bool normal = mode == "normal"; Require(mode == "blocked" || disabled || pending || normal); if (disabled || normal) seconds = 1; @@ -99,8 +126,10 @@ int main(int count, char** arguments) { Require(completed); if (normal) { Require(result == nullptr && returned.load()); + Require(cleanupCalls == 0); } else { Require(result == reinterpret_cast(std::uintptr_t{1})); + Require(cleanupCalls == 1); Require(returned.load() == disabled); if (!pending) Require(wasSleeping); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b2cbdcc526..23d865493c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -163,7 +163,7 @@ - [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 - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it - [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`), as it does here, but a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest handle storage location for the lifetime of the process; a guest that joins into a fresh storage location every time keeps every one of them. 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. `scePthreadDetach` on a joined thread answers `EINVAL`, as FreeBSD does on a thread that is not joinable; the console answer for a detach after a join is unverified. Detaching a thread while another thread waits to join it returns `EINVAL`, and a second concurrent join returns `EOPNOTSUPP`, as FreeBSD 11.4's [`thr_detach.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_detach.c) and [`thr_join.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_join.c) check `joiner`; before this PPSA34015 stopped once with `Joining guest thread: Access is denied` when a thread it was joining was released under the join, and which of the two calls released it was not captured. A detached thread is still released when it ends, so joining it after it ended reads freed memory. A handle value copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over that original storage releases an object the copy still points at, and the copy reads freed memory -- [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. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. +- [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. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after waits of at most 1 ms, retaining the GPU completion wake path. Longer waits and POSIX `sleep` wake for an enabled cancellation request. Cancellation of the SCE sleep calls remains unverified on console hardware. 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) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. 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. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. 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 - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified From 476716546ee52339aaa2ac03cebfdab3ec0817da Mon Sep 17 00:00:00 2001 From: Henryisdjfunkyho <197676650+Henryisdjfunkyho@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:19:32 -0700 Subject: [PATCH 011/702] fix(libkernel): preserve SCE timed sleep behavior --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- core/libs/prx/libkernel/Time/Time.cpp | 23 ++++++++++++++++------- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 0ef6107890..8c5e459b49 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1927,7 +1927,7 @@ if(BUILD_TESTING) ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endforeach() - foreach(sleepApi IN ITEMS usleep sce_usleep nanosleep _nanosleep sce_nanosleep sce_sleep) + foreach(sleepApi IN ITEMS usleep nanosleep _nanosleep) foreach(sleepCase IN ITEMS blocked disabled normal pending pending_long) set(sleepTest guest_sleep_cancel_${sleepApi}_${sleepCase}) add_test(NAME ${sleepTest} COMMAND guest_sleep_cancel_tests ${sleepCase} ${sleepApi}) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index cca1ab1476..11c854ba59 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -292,32 +292,41 @@ static void CancellableSleep(std::uint64_t nanos) { ThreadCancel::Check(); } +static void SleepUntilComplete(std::uint64_t nanos) { + ThreadCancel::Check(); + SleepWake::Sleep(nanos); + ThreadCancel::Check(); +} + int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds) { TraceSleep(__builtin_return_address(0), microseconds); - CancellableSleep(static_cast(microseconds) * 1000ULL); + SleepUntilComplete(static_cast(microseconds) * 1000ULL); return 0; } -static int SleepForRequest(const KernelTimespec* rqtp) { +static int SleepForRequest(const KernelTimespec* rqtp, bool cancellable) { constexpr int guestFault = 14; constexpr int guestInvalid = 22; if (rqtp == nullptr) return guestFault; if (rqtp->tv_nsec < 0 || rqtp->tv_nsec >= 1000000000LL) return guestInvalid; constexpr std::int64_t longestSleepSeconds = INT32_MAX / 2; if (rqtp->tv_sec >= 0) { - CancellableSleep(static_cast(std::min(static_cast(rqtp->tv_sec), longestSleepSeconds)) * 1000000000ULL + - static_cast(rqtp->tv_nsec)); + const auto nanos = static_cast( + std::min(static_cast(rqtp->tv_sec), longestSleepSeconds)) * + 1000000000ULL + static_cast(rqtp->tv_nsec); + if (cancellable) CancellableSleep(nanos); + else SleepUntilComplete(nanos); } return 0; } int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp) { - const int error = SleepForRequest(rqtp); + const int error = SleepForRequest(rqtp, false); return error == 0 ? 0 : SceKernelError(error); } int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp) { - const int error = SleepForRequest(rqtp); + const int error = SleepForRequest(rqtp, true); if (error == 0) return 0; *__error_nid_postfix() = error; return -1; @@ -755,7 +764,7 @@ std::int64_t APS5_VABI sceKernelGetCpuFrequency(void) { } unsigned int APS5_VABI sceKernelSleep(unsigned int seconds) { - CancellableSleep(static_cast(seconds) * 1000000000ULL); + SleepUntilComplete(static_cast(seconds) * 1000000000ULL); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 23d865493c..75e8cac258 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -163,7 +163,7 @@ - [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 - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it - [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`), as it does here, but a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest handle storage location for the lifetime of the process; a guest that joins into a fresh storage location every time keeps every one of them. 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. `scePthreadDetach` on a joined thread answers `EINVAL`, as FreeBSD does on a thread that is not joinable; the console answer for a detach after a join is unverified. Detaching a thread while another thread waits to join it returns `EINVAL`, and a second concurrent join returns `EOPNOTSUPP`, as FreeBSD 11.4's [`thr_detach.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_detach.c) and [`thr_join.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_join.c) check `joiner`; before this PPSA34015 stopped once with `Joining guest thread: Access is denied` when a thread it was joining was released under the join, and which of the two calls released it was not captured. A detached thread is still released when it ends, so joining it after it ended reads freed memory. A handle value copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over that original storage releases an object the copy still points at, and the copy reads freed memory -- [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. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after waits of at most 1 ms, retaining the GPU completion wake path. Longer waits and POSIX `sleep` wake for an enabled cancellation request. Cancellation of the SCE sleep calls remains unverified on console hardware. The asynchronous type acts like the deferred one, except at the two set calls. +- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) is not verified on console and follows FreeBSD libthr: states/types 0/1 and 0/2, canceled value 1, and cleanup node layout. Requests act only in `scePthreadTestcancel`, condition-variable, `sem`/`scePthreadSem`, `scePthreadJoin` and sleep waits, and their POSIX names; file/socket calls are not cancellation points. SCE sleep calls check before and after host sleep and keep the GPU wake path for waits at most 1 ms; longer waits do not wake on cancellation. POSIX `usleep`, `nanosleep` and `_nanosleep` check before and after; only waits longer than 1 ms wake on enabled cancellation, while shorter waits keep the GPU wake path. POSIX `sleep` checks before and after its timed wait and wakes on enabled cancellation. Async cancellation acts like deferred except at the two set calls. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. 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. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. 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 - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified From 54f8a66aaf8f07f4f5bb43bc44fbce88f3e3c59d Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:55:50 +0100 Subject: [PATCH 012/702] feat(libc): implement floating-point prefix helpers --- core/libs/CMakeLists.txt | 2 + core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/src/FloatingPrefix.cpp | 96 ++++++++ core/libs/tests/GuestFloatingPrefix.cpp | 224 ++++++++++++++++++ .../libs/tests/floating_prefix/CMakeLists.txt | 19 ++ docs/dev/debt/libc.md | 1 + 6 files changed, 343 insertions(+) create mode 100644 core/libs/prx/libc/src/FloatingPrefix.cpp create mode 100644 core/libs/tests/GuestFloatingPrefix.cpp create mode 100644 core/libs/tests/floating_prefix/CMakeLists.txt diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef468b08aa..6048b1292e 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -711,6 +711,8 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_subdirectory(tests/floating_prefix) + add_test_executable(guest_vorbis_dec_tests tests/GuestVorbisDec.cpp) target_include_directories(guest_vorbis_dec_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec libc) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d53c2d0461..6b1400033e 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -59,6 +59,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Bessel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Random.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatBuiltins.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatingPrefix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareAtomics.cpp diff --git a/core/libs/prx/libc/src/FloatingPrefix.cpp b/core/libs/prx/libc/src/FloatingPrefix.cpp new file mode 100644 index 0000000000..1e399e8514 --- /dev/null +++ b/core/libs/prx/libc/src/FloatingPrefix.cpp @@ -0,0 +1,96 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { +int APS5_VABI isspace_nid_postfix(int); +int APS5_VABI isalnum_nid_postfix(int); +int APS5_VABI isxdigit_nid_postfix(int); +int APS5_VABI iswspace_nid_postfix(std::uint32_t); +int APS5_VABI iswalnum_nid_postfix(std::uint32_t); +int APS5_VABI iswxdigit_nid_postfix(std::uint32_t); +} + +namespace { + +template struct CharacterRules; + +template<> struct CharacterRules { + static bool Space(char value) { return isspace_nid_postfix(static_cast(value)) != 0; } + static bool Payload(char value) { return isalnum_nid_postfix(static_cast(value)) != 0 || value == '_'; } + static bool Hex(char value) { return isxdigit_nid_postfix(static_cast(value)) != 0; } +}; + +template<> struct CharacterRules { + static bool Space(char16_t value) { return iswspace_nid_postfix(value) != 0; } + static bool Payload(char16_t value) { return iswalnum_nid_postfix(value) != 0 || value == '_'; } + static bool Hex(char16_t value) { return iswxdigit_nid_postfix(value) != 0; } +}; + +template +bool Matches(const TChar* input, const char* word) { + while (*word) { + if (*input != *word && *input != *word - ('a' - 'A')) return false; + ++input; + ++word; + } + return true; +} + +template +int ParsePrefix(const TChar** cursor, TChar** end) { + const TChar* const original = *cursor; + const TChar* current = original; + while (CharacterRules::Space(*current)) ++current; + const int negative = *current == '-' ? 8 : 0; + if (*current == '-' || *current == '+') ++current; + int result = negative | 1; + if (*current == 'n' || *current == 'N') { + if (Matches(current, "nan")) { + current += 3; + result = 4; + if (*current == '(') { + const TChar* payload = current + 1; + while (CharacterRules::Payload(*payload)) ++payload; + if (*payload == ')') current = payload + 1; + } + } else { + current = original; + result = 0; + } + if (end) *end = const_cast(current); + } else if (*current == 'i' || *current == 'I') { + if (Matches(current, "inf")) { + current += 3; + if (Matches(current, "inity")) current += 5; + result = negative | 3; + } else { + current = original; + result = 0; + } + if (end) *end = const_cast(current); + } else if (*current == '0' && (current[1] == 'x' || current[1] == 'X')) { + const TChar* digit = current + 2; + if (*digit == '.') ++digit; + if (CharacterRules::Hex(*digit)) { + current += 2; + result = negative | 2; + } + } + *cursor = current; + return result; +} + +} + +extern "C" { + +int APS5_VABI _Stopfx_nid_postfix(const char** cursor, char** end) { + if (cursor == nullptr || *cursor == nullptr) NotImplemented_nid_no_patch(__func__); + return ParsePrefix(cursor, end); +} + +int APS5_VABI _WStopfx_nid_postfix(const char16_t** cursor, char16_t** end) { + if (cursor == nullptr || *cursor == nullptr) NotImplemented_nid_no_patch(__func__); + return ParsePrefix(cursor, end); +} + +} diff --git a/core/libs/tests/GuestFloatingPrefix.cpp b/core/libs/tests/GuestFloatingPrefix.cpp new file mode 100644 index 0000000000..2fbadc6be7 --- /dev/null +++ b/core/libs/tests/GuestFloatingPrefix.cpp @@ -0,0 +1,224 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::exit(1); +} + +template +using Prefix = int (APS5_VABI*)(const TChar**, TChar**); + +struct Case { + const char* text; + int code; + std::size_t cursor; + bool writesEnd; +}; + +constexpr std::array Cases{ + Case{"", 1, 0, false}, Case{"12.5e4", 1, 0, false}, Case{".", 1, 0, false}, + Case{"-", 9, 1, false}, Case{"+", 1, 1, false}, Case{" -12", 9, 3, false}, + Case{"\t\r\n\v\f +0x1p2", 2, 9, false}, Case{"0x1", 2, 2, false}, + Case{"-0X.8p2", 10, 3, false}, Case{"0x", 1, 0, false}, Case{"0x.", 1, 0, false}, + Case{"0xg", 1, 0, false}, Case{"0x.1", 2, 2, false}, Case{"0x..1", 1, 0, false}, + Case{"0x 1", 1, 0, false}, Case{" -0xg", 9, 3, false}, + Case{"n", 0, 0, true}, Case{"na", 0, 0, true}, Case{"nax", 0, 0, true}, + Case{" -Na", 0, 0, true}, Case{"nan", 4, 3, true}, Case{"-NaN", 4, 4, true}, + Case{"nan()", 4, 5, true}, Case{"nan(a_Z09)", 4, 10, true}, + Case{"nan(payload", 4, 3, true}, Case{"nan(a-b)", 4, 3, true}, + Case{"nan(a(b))", 4, 3, true}, Case{"nan()tail", 4, 5, true}, + Case{"i", 0, 0, true}, Case{"in", 0, 0, true}, Case{"inx", 0, 0, true}, + Case{" -iN", 0, 0, true}, Case{"inf", 3, 3, true}, Case{"-INF", 11, 4, true}, + Case{"infinity", 3, 8, true}, Case{"+InFiNiTy!", 3, 9, true}, + Case{"infi", 3, 3, true}, Case{"infin", 3, 3, true}, Case{"infinit", 3, 3, true}, + Case{"infinite", 3, 3, true}, Case{" -infinity", 11, 11, true}, + Case{"not-a-number", 0, 0, true}, Case{"garbage", 1, 0, false}, +}; + +template +static std::vector Text(const char* source) { + std::vector result; + do result.push_back(static_cast(*source)); while (*source++); + return result; +} + +template +static void CheckOne(Prefix function, const TChar* text, const Case& test) { + constexpr std::uint64_t guard = 0xd3c4b5a69788796a; + TChar sentinel{}; + struct Outputs { + std::uint64_t before; + const TChar* cursor; + std::uint64_t middle; + TChar* end; + std::uint64_t after; + }; + static_assert(offsetof(Outputs, cursor) == 8 && offsetof(Outputs, middle) == 16); + static_assert(offsetof(Outputs, end) == 24 && offsetof(Outputs, after) == 32); + Outputs outputs{guard, text, guard, &sentinel, guard}; + const int code = function(&outputs.cursor, &outputs.end); + if (code != test.code || outputs.cursor != text + test.cursor || + outputs.end != (test.writesEnd ? text + test.cursor : &sentinel)) { + std::fprintf(stderr, "prefix '%s': code %d expected %d, cursor %td expected %zu\n", + test.text, code, test.code, outputs.cursor - text, test.cursor); + std::exit(1); + } + Require(outputs.before == guard && outputs.middle == guard && outputs.after == guard, "output bounds"); + const TChar* cursor = text; + Require(function(&cursor, nullptr) == test.code && cursor == text + test.cursor, "optional end pointer"); +} + +template +static void CheckUnsupported(Prefix function) { + TChar sentinel{}; + TChar* end = &sentinel; + const TChar* cursor = nullptr; + for (const bool nullSlot : {false, true}) { + bool threw = false; + try { + function(nullSlot ? nullptr : &cursor, &end); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && cursor == nullptr && end == &sentinel, "null input must remain explicitly unsupported"); + } +} + +template +static void Check(Prefix function, unsigned payloadLimit = 256) { + for (const auto& test : Cases) { + auto text = Text(test.text); + const auto saved = text; + CheckOne(function, text.data(), test); + Require(text == saved, "input modified"); + } + for (const char* word : {"nan", "inf", "infinity"}) { + const std::size_t length = std::strlen(word); + for (unsigned mask = 0; mask < (1u << length); ++mask) { + std::string text = word; + for (std::size_t index = 0; index < length; ++index) + if (mask & (1u << index)) text[index] -= 'a' - 'A'; + for (const char* sign : {"", "+", "-"}) { + const std::string input = " \t" + std::string(sign) + text; + const int code = word[0] == 'n' ? 4 : 3 + (sign[0] == '-' ? 8 : 0); + const Case test{input.c_str(), code, input.size(), true}; + const auto data = Text(input.c_str()); + CheckOne(function, data.data(), test); + } + } + } + for (unsigned value = 0; value < payloadLimit; ++value) { + const std::array payload{TChar{'n'}, TChar{'a'}, TChar{'n'}, TChar{'('}, static_cast(value), TChar{')'}, TChar{}}; + const bool accepted = (value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z') || + (value >= '0' && value <= '9') || value == '_'; + const std::size_t stop = accepted ? 6 : value == ')' ? 5 : 3; + const std::string label = "payload byte " + std::to_string(value) + ", unit width " + std::to_string(sizeof(TChar)); + CheckOne(function, payload.data(), Case{label.c_str(), 4, stop, true}); + } + if constexpr (sizeof(TChar) == 2) { + const std::array high{static_cast(0x100), TChar{'i'}, TChar{'n'}, TChar{}}; + CheckOne(function, high.data(), Case{"wide code unit", 1, 0, false}); + } +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const std::size_t page = info.dwPageSize; + auto* allocation = static_cast(VirtualAlloc(nullptr, page * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(allocation != nullptr, "guard allocation"); + DWORD previous = 0; + Require(VirtualProtect(allocation + page, page, PAGE_NOACCESS, &previous) != 0, "guard protection"); +#else + const std::size_t page = static_cast(sysconf(_SC_PAGESIZE)); + auto* allocation = static_cast(mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED, "guard allocation"); + Require(mprotect(allocation + page, page, PROT_NONE) == 0, "guard protection"); +#endif + for (const auto& test : Cases) { + const auto source = Text(test.text); + const std::size_t bytes = source.size() * sizeof(TChar); + auto* text = reinterpret_cast(allocation + page - bytes); + std::memcpy(text, source.data(), bytes); + CheckOne(function, text, test); + } +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0, "guard release"); +#else + Require(munmap(allocation, page * 2) == 0, "guard release"); +#endif +} + +template +static void CheckConcurrent(Prefix function) { + std::array workers; + for (auto& worker : workers) { + worker = std::thread([function] { + for (unsigned repeat = 0; repeat < 8; ++repeat) { + for (const auto& test : Cases) { + const auto text = Text(test.text); + CheckOne(function, text.data(), test); + } + } + }); + } + for (auto& worker : workers) worker.join(); +} + +int main(int argc, char** argv) { + Require(argc == 4, "library directory, patched/unpatched, narrow/wide"); + const bool wide = std::string(argv[3]) == "wide"; + Require(wide || std::string(argv[3]) == "narrow", "width selection"); + const std::string name = wide ? "_WStopfx" : "_Stopfx"; + const std::string symbol = std::string(argv[2]) == "patched" ? Nid::ComputeNid(name, "") : name + "_nid_postfix"; + const auto path = std::filesystem::absolute(argv[1]) / "libc.prx"; +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + const auto startup = GetModuleHandleW(L"libc.prx"); + const auto library = LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + Require(library != nullptr, "load libc"); + std::array loaded{}; + const auto length = GetModuleFileNameW(library, loaded.data(), static_cast(loaded.size())); + Require(length != 0 && length < loaded.size() && std::filesystem::equivalent(path, std::filesystem::path(loaded.data())), "wrong libc loaded"); + if (startup) Require(startup == library, "startup libc differs"); + const auto function = reinterpret_cast(GetProcAddress(library, symbol.c_str())); +#else + const auto library = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!library) std::fprintf(stderr, "%s\n", dlerror()); + Require(library != nullptr, "load libc"); + const auto function = dlsym(library, symbol.c_str()); +#endif + if (!function) std::fprintf(stderr, "Missing %s (%s)\n", name.c_str(), symbol.c_str()); + Require(function != nullptr, "cannot exercise missing prefix parser"); + if (wide) { + const auto prefix = reinterpret_cast>(function); + Check(prefix); + CheckUnsupported(prefix); + CheckConcurrent(prefix); + } else { + const auto prefix = reinterpret_cast>(function); + Check(prefix); + CheckUnsupported(prefix); + CheckConcurrent(prefix); + } + std::printf("%s: prefix codes, pointer updates, input/output bounds and guarded reads passed\n", name.c_str()); + return 0; +} diff --git a/core/libs/tests/floating_prefix/CMakeLists.txt b/core/libs/tests/floating_prefix/CMakeLists.txt new file mode 100644 index 0000000000..32c8435992 --- /dev/null +++ b/core/libs/tests/floating_prefix/CMakeLists.txt @@ -0,0 +1,19 @@ +add_test_executable(guest_floating_prefix_tests ${LIBS_INCLUDE_DIR}/tests/GuestFloatingPrefix.cpp + ${LIBS_INCLUDE_DIR}/nid/src/NidCompute.cpp ${LIBS_INCLUDE_DIR}/nid/src/Sha1.cpp) +target_include_directories(guest_floating_prefix_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) +target_link_libraries(guest_floating_prefix_tests PRIVATE ${CMAKE_DL_LIBS}) +configure_windows_unwind(guest_floating_prefix_tests) +if(MINGW) + target_link_libraries(guest_floating_prefix_tests PRIVATE libc) +endif() +add_custom_target(guest_floating_prefix_libraries DEPENDS ${LIBC_PATCHED}) +add_dependencies(guest_floating_prefix_tests guest_floating_prefix_libraries) +foreach(width IN ITEMS narrow wide) + add_test(NAME guest_floating_prefix_${width} COMMAND guest_floating_prefix_tests $ unpatched ${width}) + add_test(NAME guest_floating_prefix_${width}_patched COMMAND guest_floating_prefix_tests ${PATCHED_DIR} patched ${width}) + set_tests_properties(guest_floating_prefix_${width} guest_floating_prefix_${width}_patched PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_floating_prefix_${width} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_floating_prefix_${width}_patched PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${PATCHED_DIR}") + endif() +endforeach() diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 5014c4dd6f..152fe2be60 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -129,6 +129,7 @@ - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) - [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds +- [_Stopfx and _WStopfx](../../../core/libs/prx/libc/src/FloatingPrefix.cpp) follow the prefix results and pointer updates of Dinkumware V6.00 [xstopfx.c](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstopfx.c) and [xwstopfx.c](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xwstopfx.c), without console verification. Sony signatures, module/library versions and locale equivalence are unverified; the wide code units follow the existing 16-bit libc string ABI, and character classification inherits the guest ctype/rune limitations. The guest default rune table rejects non-ASCII NaN payload letters that native Windows `iswalnum` accepts, including U+00AA in its C locale; console classification is unmeasured. A null cursor or input pointer throws; console invalid-pointer behavior is unknown. These helpers classify prefixes and do not convert numbers or consume their digits. - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified From a1cd1eba21b28457837383c38fe499274c5feb4c Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:03:41 +0100 Subject: [PATCH 013/702] build(libc): avoid unrelated shader test registration overlap --- core/libs/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 6048b1292e..d4757c9ca1 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -671,6 +671,8 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_subdirectory(tests/floating_prefix) + add_test_executable(MemoryPoolTest tests/MemoryPoolTest.cpp prx/libkernel/DirectMemory/MemoryPool.cpp @@ -711,8 +713,6 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() - add_subdirectory(tests/floating_prefix) - add_test_executable(guest_vorbis_dec_tests tests/GuestVorbisDec.cpp) target_include_directories(guest_vorbis_dec_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec libc) From 50e9cc40218e324eb3ee371ac1234c83093f90b5 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 18:50:15 +0300 Subject: [PATCH 014/702] perf(recompiler): translate GCN buffer formats without a private table Registration modules read a GCN V#'s word 3 in the shader, and gcnWord3 mapped its DATA_FORMAT and NUM_FORMAT through a 128-entry Private array that every buffer access stored again before indexing it. Drivers keep that array in per-invocation scratch: on the RX 9070 XT each pixel shader of Monopoly Plus's language screen used 528 bytes of scratch and wrote the table up to 60 times per invocation. The lookup is now a helper function that packs the table four entries to a word and picks the word with a select tree, so it stays in registers and each access calls it once. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../tests/execution/GcnBufferFormatLookup.cpp | 211 ++++++++++++++++++ .../SpirvBackend/SpirvEmitterState.hpp | 2 +- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 54 ++++- 4 files changed, 257 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2bb2a650aa..746e8f0366 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2200,6 +2200,7 @@ target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $) target_include_directories(agc_driver_failed_capture_dump_tests PRIVATE $) +target_include_directories(agc_driver_gcn_buffer_format_lookup_tests PRIVATE $) target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp new file mode 100644 index 0000000000..1254599280 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp @@ -0,0 +1,211 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "RdnaDecoder/RdnaDescriptorFormat.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.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::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t Vertices = 3; +constexpr std::uint32_t Fill = 0xdeadbeefu; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uintptr_t GcnAddressLimit = std::uintptr_t{1} << 44u; +constexpr std::array InputWords{0x12345678u, 0x9abcdef0u, 0x0badf00du, 0x600dcafeu}; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0300000u, 0x80010100u, 0xbf8c0f70u, 0xe0700000u, 0x80020100u, 0xf80008cfu, 0x03020100u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u, +}; + +class LowBlock { +public: + LowBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + void* mapped = mmap(reinterpret_cast(std::uintptr_t{1} << 36u), BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + block = mapped == MAP_FAILED ? nullptr : static_cast(mapped); +#endif + Require(block != nullptr, "GCN buffer format lookup: cannot allocate the buffer block"); + Require(reinterpret_cast(block) + BlockBytes <= GcnAddressLimit, "GCN buffer format lookup: the buffer block lies above the 44-bit GCN V# address range"); + } + + ~LowBlock() { +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, BlockBytes); +#endif + } + + LowBlock(const LowBlock&) = delete; + LowBlock& operator=(const LowBlock&) = delete; + + std::span Input() const { return std::span(block, 4); } + std::span Output() const { return std::span(block + 64, 4); } + +private: + std::uint32_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes, std::uint32_t dataFormat, std::uint32_t numberFormat) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0xfacu | (numberFormat << 12u) | (dataFormat << 15u)}; +} + +std::vector UserData(const LowBlock& block, std::uint32_t dataFormat, std::uint32_t numberFormat) { + std::vector userData(12, 0u); + const auto input = BufferDescriptor(block.Input().data(), 16u, dataFormat, numberFormat); + const auto output = BufferDescriptor(block.Output().data(), 16u, 4u, 4u); + std::copy(input.begin(), input.end(), userData.begin() + 4); + std::copy(output.begin(), output.end(), userData.begin() + 8); + return userData; +} + +ShaderRecompiler::RecompileRequest VertexRequest(std::span userData, const ShaderRecompiler::SpirvTarget& target, std::span memory) { + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64u, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, memory}, + target, + {0, 0, 0, 64} + }; + request.useCache = false; + return request; +} + +std::uint32_t PrivateStores(std::span words) { + std::set privateMemory; + std::uint32_t stores = 0; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Require(count != 0 && count <= words.size() - offset, "GCN buffer format lookup: malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpVariable && count >= 4 && words[offset + 3] == spv::StorageClassPrivate) privateMemory.insert(words[offset + 2]); + if ((opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain) && count >= 4 && privateMemory.contains(words[offset + 3])) privateMemory.insert(words[offset + 2]); + if (opcode == spv::OpStore && count >= 3 && privateMemory.contains(words[offset + 1])) ++stores; + offset += count; + } + return stores; +} + +void CheckModule(const AgcDriver::VulkanDevice& device, const LowBlock& block, std::span memory) { + auto target = device.Target(); + target.registrationModules = true; + const auto userData = UserData(block, 4u, 4u); + const auto result = ShaderRecompiler::Recompile(VertexRequest(userData, target, memory)); + Require(result.registrationModule, "GCN buffer format lookup: the vertex shader was not emitted in registration form"); + const auto stores = PrivateStores(result.spirv.Words()); + Require(stores == 0u, "GCN buffer format lookup: the registration module stores " + std::to_string(stores) + " times into private memory to translate DATA_FORMAT and NUM_FORMAT, which drivers keep in per-invocation scratch"); +} + +void Draw(AgcDriver::VulkanDevice& device, const LowBlock& block, std::uint32_t dataFormat, std::uint32_t numberFormat, std::span vertexMemory, const ShaderRecompiler::RecompileResult& pixelResult, std::uint32_t vertexPush, std::vector& registrationModule) { + Pixels.fill(std::byte{0x40}); + std::copy(InputWords.begin(), InputWords.end(), block.Input().begin()); + std::fill(block.Output().begin(), block.Output().end(), Fill); + const auto userData = UserData(block, dataFormat, numberFormat); + const auto vertexResult = ShaderRecompiler::Recompile(VertexRequest(userData, device.Target(), vertexMemory)); + const auto what = "DATA_FORMAT " + std::to_string(dataFormat) + " NUM_FORMAT " + std::to_string(numberFormat); + Require(static_cast(vertexResult.pushConstants.size()) == vertexPush, "GCN buffer format lookup, " + what + ": the vertex push constants changed size"); + if (device.Target().registrationModules) { + if (registrationModule.empty()) registrationModule = vertexResult.spirv.Copy(); + Require(std::ranges::equal(vertexResult.spirv.Words(), registrationModule), "GCN buffer format lookup, " + what + ": registration modules: the V# specialized the vertex module"); + } + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, 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}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, Vertices, 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + const auto translated = ShaderRecompiler::GcnBufferFormats()[(dataFormat << 3u) | numberFormat]; + const auto expected = translated != 0u ? InputWords[0] : 0u; + const auto output = block.Output(); + char text[96]; + std::snprintf(text, sizeof(text), ": translated format %u, loaded 0x%08x, expected 0x%08x", translated, output[0], expected); + Require(output[0] == expected, "GCN buffer format lookup, " + what + text); + Require(std::all_of(output.begin() + 1, output.end(), [](std::uint32_t value) { return value == Fill; }), "GCN buffer format lookup, " + what + ": the store wrote past its dword"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + ShaderRecompiler::SetGcnGuestShaders(true); + const LowBlock block; + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + CheckModule(*device, block, vertexMemory); + const auto firstUserData = UserData(block, 4u, 4u); + const auto vertexPush = static_cast(ShaderRecompiler::Recompile(VertexRequest(firstUserData, device->Target(), vertexMemory)).pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + 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(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + device->Target(), + PixelPushLayout(vertexPush, device->Target()) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + std::vector registrationModule; + for (std::uint32_t dataFormat = 0; dataFormat < 16u; ++dataFormat) { + for (std::uint32_t numberFormat = 0; numberFormat < 8u; ++numberFormat) Draw(*device, block, dataFormat, numberFormat, vertexMemory, pixelResult, vertexPush, registrationModule); + } + std::puts("GCN buffer format lookup tests passed"); + 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 92ea828d77..57020370da 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -187,7 +187,7 @@ struct SpirvEmitterState { std::uint32_t loopGuardVisits = 0; std::uint32_t loopGuardPc = 0; std::uint32_t srgbTableVariable = 0; - std::uint32_t gcnBufferFormatVariable = 0; + std::uint32_t gcnBufferFormatFunction = 0; std::uint32_t gdsVariable = 0; std::uint32_t gdsLength = 0; std::uint32_t pushConstantVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index b71a38a1c1..130481696d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -50,18 +50,52 @@ std::uint32_t nonzero(SpirvEmitterState& state, std::uint32_t value) { return Binary(state, spv::OpINotEqual, TypeBool(state), value, ConstantU32(state, 0u)); } +std::uint32_t gcnBufferFormat(SpirvEmitterState& state, std::uint32_t index) { + const auto u32 = TypeU32(state); + if (state.gcnBufferFormatFunction == 0u) { + const auto id = state.module.AllocateId(); + struct Restore { + SpirvEmitterState& state; + std::uint32_t label; + ~Restore() { + state.module.EndHelperFunction(); + state.currentLabel = label; + } + }; + state.module.BeginHelperFunction(); + const Restore restore{state, state.currentLabel}; + state.module.AddName(id, "gcn_buffer_format"); + state.module.AddFunction(spv::OpFunction, u32, id, spv::FunctionControlMaskNone, state.module.Type(spv::OpTypeFunction, u32, u32)); + const auto parameter = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u32, parameter); + EmitLabel(state, state.module.AllocateId()); + const auto& formats = GcnBufferFormats(); + std::vector words; + for (std::size_t entry = 0; entry < formats.size(); entry += 4u) { + std::uint32_t packed = 0; + for (std::uint32_t byte = 0; byte < 4u; ++byte) packed |= static_cast(formats[entry + byte]) << (byte * 8u); + words.push_back(ConstantU32(state, packed)); + } + const auto word = Binary(state, spv::OpShiftRightLogical, u32, parameter, ConstantU32(state, 2u)); + for (std::uint32_t bit = 0; words.size() > 1u; ++bit) { + const auto odd = nonzero(state, Binary(state, spv::OpBitwiseAnd, u32, word, ConstantU32(state, 1u << bit))); + std::vector halves; + for (std::size_t pair = 0; pair < words.size(); pair += 2u) halves.push_back(words[pair] == words[pair + 1u] ? words[pair] : Select(state, u32, odd, words[pair + 1u], words[pair])); + words = std::move(halves); + } + const auto shift = Binary(state, spv::OpShiftLeftLogical, u32, Binary(state, spv::OpBitwiseAnd, u32, parameter, ConstantU32(state, 3u)), ConstantU32(state, 3u)); + state.module.AddFunction(spv::OpReturnValue, EmitBitFieldUExtract(state, words.front(), shift, ConstantU32(state, 8u))); + state.module.AddFunction(spv::OpFunctionEnd); + state.gcnBufferFormatFunction = id; + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, u32, result, state.gcnBufferFormatFunction, index); + return result; +} + std::uint32_t gcnWord3(SpirvEmitterState& state, std::uint32_t word1, std::uint32_t word3) { const auto u32 = TypeU32(state); - const auto& formats = GcnBufferFormats(); - std::array words{}; - for (std::size_t index = 0; index < words.size(); ++index) words[index] = ConstantU32(state, formats[index]); - const auto arrayType = state.module.Type(spv::OpTypeArray, u32, ConstantU32(state, static_cast(words.size()))); - if (state.gcnBufferFormatVariable == 0) state.gcnBufferFormatVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate); - state.module.AddFunction(spv::OpStore, state.gcnBufferFormatVariable, state.module.Constant(spv::OpConstantComposite, arrayType, std::span(words))); - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, u32), pointer, state.gcnBufferFormatVariable, field(state, word3, 12u, 7u)); - const auto format = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, u32, format, pointer); + const auto format = gcnBufferFormat(state, field(state, word3, 12u, 7u)); const auto outOfBounds = Select(state, u32, nonzero(state, field(state, word1, 16u, 14u)), ConstantU32(state, 1u << 28u), ConstantU32(state, 3u << 28u)); const auto kept = Binary(state, spv::OpBitwiseAnd, u32, word3, ConstantU32(state, 0xc0e00fffu)); return Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseOr, u32, kept, Binary(state, spv::OpShiftLeftLogical, u32, format, ConstantU32(state, 12u))), outOfBounds); From 6d85dd2b5ba9bec105e12581acfa1eeb245bda6a Mon Sep 17 00:00:00 2001 From: Berrick Werkman <105836264+BKlaasWerkman@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:38:36 +0200 Subject: [PATCH 015/702] fix(shader,agc): declare ImageMSArray only for multisampled arrayed storage images SPIR-V requires ImageMSArray only for an OpTypeImage with MS 1, Arrayed 1 and Sampled 2, and Vulkan requires shaderStorageImageMultisample for a module that declares it. The recompiler declared it for every multisampled arrayed image, including sampled ones, and the device listed it as supported without the feature, so a sampled Dim2DMsaaArray load produced an invalid module on devices without shaderStorageImageMultisample (VUID-VkShaderModuleCreateInfo-pCode-08740). Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/src/VulkanDevice.cpp | 6 ++++-- .../tests/execution/SampleMultisampleTarget.cpp | 12 ++++++++++++ .../src/SpirvMemory/SpirvDescriptors.cpp | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 9d93a5127f..3eeead2391 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1706,8 +1706,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Recompiled storage-image access declares no format (the guest descriptor decides it). enabled.shaderStorageImageMultisample = available.shaderStorageImageMultisample; state->storageImageMultisample = enabled.shaderStorageImageMultisample == VK_TRUE; - if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample); - state->capabilities.push_back(spv::CapabilityImageMSArray); + if (enabled.shaderStorageImageMultisample) { + state->capabilities.push_back(spv::CapabilityStorageImageMultisample); + state->capabilities.push_back(spv::CapabilityImageMSArray); + } enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat; state->storageImageWriteWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE; enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index abe81b5503..2b15e473ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -15,6 +15,7 @@ #include "Recompiler.hpp" #include "Optimization/ResourceProgram.hpp" #include "VulkanTestDevice.hpp" +#include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -382,9 +383,20 @@ void RequireStoreRefused(AgcDriver::VulkanDevice& device, const std::array words, spv::Capability capability) { + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + Require(count != 0u && count <= words.size() - cursor, "a SPIR-V instruction is truncated"); + if ((words[cursor] & 0xffffu) == spv::OpCapability && words[cursor + 1u] == static_cast(capability)) return true; + cursor += count; + } + return false; +} + std::vector> RunLoad(AgcDriver::VulkanDevice& device, const std::array& texture, bool arrayed, std::uint32_t fragments) { std::vector userData; const auto program = CompileLoad(device, texture, userData, arrayed); + Require(!DeclaresCapability(program.spirv, spv::CapabilityImageMSArray), "the multisampled image_load declared ImageMSArray, which only multisampled arrayed storage images need"); std::vector> loaded(Side * Side * fragments); const auto codeAddress = reinterpret_cast((arrayed ? ArrayLoadCode : LoadCode).data()); for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 946baa6068..e47801c61e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -137,8 +137,8 @@ std::uint32_t ImageType(SpirvEmitterState& state, const ImageResource& image) { if (sampled == 2u) { if (std::ranges::find(state.supportedCapabilities, spv::CapabilityStorageImageMultisample) == state.supportedCapabilities.end()) FailEmit("storage image multisampling is unavailable on the target device"); state.module.EmitCapability(spv::CapabilityStorageImageMultisample); + if (info.arrayed != 0u) state.module.EmitCapability(spv::CapabilityImageMSArray); } - if (info.arrayed != 0u) state.module.EmitCapability(spv::CapabilityImageMSArray); } const auto scalar = image.atomic64 ? TypeScalarU64(state) : ImageScalarType(state, image.numericClass); return state.module.Type(spv::OpTypeImage, scalar, info.spirvDimension, image.depthCompare ? 1u : 0u, info.arrayed, info.multisampled, sampled, format); From 7ec9ce913255bdac2d369e81d0a0560082082852 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 19 Oct 2026 16:51:50 +1300 Subject: [PATCH 016/702] fix(agc): clamp an exported pixel depth to the viewport depth range The DB clamps a pixel shader's exported depth to PA_SC_VPORT_ZMIN_0 and ZMAX_0 unless DB_RENDER_OVERRIDE.DISABLE_VIEWPORT_CLAMP is set. Vulkan only clamps with depthClamp, which also drops clipping, so the exported value reached the depth attachment unclamped. The pixel stage info now carries the two bounds when the shader exports depth (0 and 1 otherwise), and the exported value is NClamped to them before it is stored to gl_FragDepth. With DISABLE_VIEWPORT_CLAMP the value is stored as exported; non-finite or inverted bounds are rejected. DB_RENDER_OVERRIDE joins the draw key registers. The bounds are part of the recompile cache key (only for depth-exporting shaders, so other keys do not change), the draw key and its register check, and recompile requests (serialization version 47). Tests: the pixel stage decode reads the bounds, passes the disable flag through and rejects bad bounds, and leaves a draw without depth export at 0 and 1; a recompiled depth export stores the NClamped value, and its bounds survive serialization and key the cache. Request version 47: main is at 44 (#8612) and open PRs also claim 44 to 46 (#8564, #7888, #9193 and the #9020, #9335, #9748 stack). Whichever of these lands second renumbers. --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 2 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Graphics/include/State.hpp | 2 +- .../Graphics/src/ShaderInputState.cpp | 22 +++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 24 +++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 87 ++++++++++++++++++- .../tests/execution/DepthExportClamp.cpp | 75 ++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 4 + .../ControlFlow/src/RequestSerializer.cpp | 12 ++- .../Optimization/ShaderStageInputInfo.hpp | 2 + core/shader/recompiler/Recompiler.hpp | 2 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 10 ++- .../src/ShaderInputInfoBuilder.cpp | 2 + .../tests/RequestSerializerTests.cpp | 22 ++--- 14 files changed, 249 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthExportClamp.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 5cb9982751..b77ec4a6f6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -74,6 +74,8 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.depthExportMin)); + mix(std::bit_cast(pixel.depthExportMax)); 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 288ce32804..21d4b1a4c5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -255,7 +255,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.frontFaceAllBits != q.frontFaceAllBits || 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.stencilOperationValueExport != q.stencilOperationValueExport || 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.frontFaceAllBits != q.frontFaceAllBits || 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.depthExportMin != q.depthExportMin || p.depthExportMax != q.depthExportMax || 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.stencilOperationValueExport != q.stencilOperationValueExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false; for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 2008d77be3..42d5006977 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -277,7 +277,7 @@ struct DrawKeyRange { std::uint32_t count; }; inline constexpr std::array DrawKeyRegisters{{ - {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5}, + {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 2}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL .. SPI_BARYC_CNTL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 30ea580803..08278405d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -6,6 +6,9 @@ #include "prx/libc/include/HostThreadLocal.hpp" #include #include +#include +#include +#include #include #include #include @@ -27,6 +30,9 @@ constexpr std::uint32_t spiPsInputAddr = 0x1B4; constexpr std::uint32_t spiPsInControl = 0x1B6; constexpr std::uint32_t spiBarycCntl = 0x1B8; constexpr std::uint32_t dbShaderControl = 0x203; +constexpr std::uint32_t dbRenderOverride = 0x003; +constexpr std::uint32_t paScVportZmin0 = 0x0B4; +constexpr std::uint32_t paScVportZmax0 = 0x0B5; constexpr std::uint32_t spiShaderColFormat = 0x1C5; constexpr std::uint32_t defaultPixelInputs = 0x2u; @@ -143,6 +149,20 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con } const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0; const bool depthExportEnable = (shaderControl & 0x1u) != 0; + float depthExportMin = 0.0f; + float depthExportMax = 1.0f; + if (depthExportEnable) { + if ((read(context, dbRenderOverride, RegisterBank::Context) & 0x10000u) != 0) { + depthExportMin = -std::numeric_limits::infinity(); + depthExportMax = std::numeric_limits::infinity(); + } else { + depthExportMin = std::bit_cast(read(context, paScVportZmin0, RegisterBank::Context)); + depthExportMax = std::bit_cast(read(context, paScVportZmax0, RegisterBank::Context)); + if (!std::isfinite(depthExportMin) || !std::isfinite(depthExportMax) || depthExportMin > depthExportMax) { + throw std::runtime_error("AGC graphics: invalid viewport depth clamp bounds for depth export"); + } + } + } const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; const bool stencilOperationValue = !nullProgram && StencilOperationValueReplaces(context); const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0 || stencilOperationValue; @@ -176,6 +196,8 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .linearCentroid = loaded(PixelInput::LinearCentroid), .pixelKillEnable = pixelKillEnable, .depthExportEnable = depthExportEnable, + .depthExportMin = depthExportMin, + .depthExportMax = depthExportMax, .sampleMaskExportEnable = sampleMaskExportEnable, .earlyZ = PixelEarlyZ(shaderControl), .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7b4e5bda3d..8d502d2939 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -31,6 +31,7 @@ #include "prx/libc/include/GuestHeap.hpp" #include "ImageTableAbi.hpp" #include "SpirvBackend/SpirvSpecialization.hpp" +#include #include #ifdef _WIN32 #include @@ -105,6 +106,7 @@ AgcDriver::QueueState makeState() { queue.context[0x114] = 0; queue.context[0xb4] = 0; queue.context[0xb5] = std::bit_cast(1.0f); + queue.context[0x3] = 0; queue.shader[0x008] = 0x100; queue.shader[0x009] = 0; return queue; @@ -315,6 +317,9 @@ void retainedPixelStageTests() { Require(!PixelProgramSkipped(queue), "an enabled high render target lost its pixel program"); queue.context[0x8e] = queue.context[0x8f] = 0; } + queue.context[0x3] = 0; + queue.context[0xb4] = std::bit_cast(0.0f); + queue.context[0xb5] = std::bit_cast(1.0f); queue.context[0x1b3] = queue.context[0x1b4] = 2; for (const auto [format, control] : {std::pair{1u, 1u}, std::pair{2u, 2u}, std::pair{9u, 0x100u}, std::pair{9u, 0x103u}}) { queue.context[0x1c4] = format; @@ -324,6 +329,25 @@ void retainedPixelStageTests() { const auto pixel = DecodePixelStageInfo(queue.context, ExportMappings(state), PixelProgramSkipped(queue)); Require(pixel.depthExportEnable == ((control & 1u) != 0) && pixel.stencilRefExportEnable == ((control & 2u) != 0) && pixel.sampleMaskExportEnable == ((control & 0x100u) != 0), "decoding lost an enabled pixel export"); } + queue.context[0x1c4] = 1; + queue.context[0x203] = 1; + queue.context[0xb4] = std::bit_cast(0.25f); + queue.context[0xb5] = std::bit_cast(0.75f); + const auto clampState = DecodeState(queue); + const auto clamped = DecodePixelStageInfo(queue.context, ExportMappings(clampState)); + Require(clamped.depthExportEnable && clamped.depthExportMin == 0.25f && clamped.depthExportMax == 0.75f, "PA_SC_VPORT_ZMIN_0/ZMAX_0 did not become the exported depth clamp"); + queue.context[0x3] = 0x10000; + const auto unclamped = DecodePixelStageInfo(queue.context, ExportMappings(clampState)); + Require(unclamped.depthExportEnable && unclamped.depthExportMin == -std::numeric_limits::infinity() && unclamped.depthExportMax == std::numeric_limits::infinity(), "DB_RENDER_OVERRIDE.DISABLE_VIEWPORT_CLAMP did not leave the exported depth unclamped"); + queue.context[0x3] = 0; + queue.context[0xb4] = std::bit_cast(0.9f); + expectFailure([&] { static_cast(DecodePixelStageInfo(queue.context, ExportMappings(clampState))); }, "invalid viewport depth clamp bounds"); + queue.context[0xb4] = std::bit_cast(std::numeric_limits::quiet_NaN()); + expectFailure([&] { static_cast(DecodePixelStageInfo(queue.context, ExportMappings(clampState))); }, "invalid viewport depth clamp bounds"); + queue.context[0x1c4] = 0; + queue.context[0x203] = 0x10; + const auto notExporting = DecodePixelStageInfo(queue.context, ExportMappings(clampState)); + Require(!notExporting.depthExportEnable && notExporting.depthExportMin == 0.0f && notExporting.depthExportMax == 1.0f, "a draw without depth export took the viewport depth clamp"); queue.context[0x203] = 0; for (const auto alpha : {0xaa00u, 0xaa01u}) { queue.context[0x2dc] = alpha; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index e206810771..88273ae11e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -29,6 +29,7 @@ #include "BdaAbi.hpp" #include "ImageTableAbi.hpp" #include "ShaderDiskCache.hpp" +#include #include #include #include @@ -4944,7 +4945,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQSwAAAA=", "new requests did not use serialization version 44"); + require(requestPrefix(encoded, 8u) == "NVNQQS8AAAA=", "new requests did not use serialization version 47"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -5062,7 +5063,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQS0AAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQTAAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5134,6 +5135,85 @@ std::string encodeBase64(const std::vector& bytes) { return text; } +void verifyDepthExportClamp() { + using namespace ShaderRecompiler; + static constexpr std::array code{0xf8001881u, 0x00000000u, 0xbf810000u}; + ShaderPixelStageInfo pixel{}; + pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + pixel.hasPerspectiveCenterVgpr = true; + pixel.depthExportEnable = true; + pixel.depthExportMin = 0.25f; + pixel.depthExportMax = 0.75f; + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + const auto result = Recompile(request); + const auto& words = result.spirv.Words(); + std::map floats; + std::vector> clamps; + std::vector stored; + std::uint32_t depthVariable = 0; + bool depthReplacing = false; + 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::OpConstant && (words[at] >> 16u) == 4u) floats[words[at + 2]] = words[at + 3]; + if (op == spv::OpExtInst && words[at + 4] == 81u) clamps.push_back({words[at + 2], words[at + 6], words[at + 7]}); + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInFragDepth) depthVariable = words[at + 1]; + if (op == spv::OpStore) stored.push_back(words[at + 1]), stored.push_back(words[at + 2]); + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeDepthReplacing) depthReplacing = true; + } + require(depthVariable != 0u && depthReplacing, "depth export: the pixel shader does not replace gl_FragDepth"); + require(clamps.size() == 1u && floats[clamps[0][1]] == 0x3e800000u && floats[clamps[0][2]] == 0x3f400000u, "depth export: the exported depth was not NClamped to [ZMIN, ZMAX]"); + bool clampStored = false; + for (std::size_t i = 0; i + 1 < stored.size(); i += 2) clampStored = clampStored || (stored[i] == depthVariable && stored[i + 1] == clamps[0][0]); + require(clampStored, "depth export: gl_FragDepth was not written from the clamped value"); + const auto replay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(request)); + require(replay.request.context.pixel->depthExportMin == 0.25f && replay.request.context.pixel->depthExportMax == 0.75f, "depth export: the clamp did not survive serialization"); + std::vector key; + RecompileCacheKey::Build(request, key); + const auto first = key; + auto other = request; + auto changed = pixel; + changed.depthExportMax = 1.0f; + other.context.pixel = changed; + RecompileCacheKey::Build(other, key); + require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "depth export: the cache keys ignore the clamp"); + auto plain = pixel; + plain.depthExportEnable = false; + auto unexported = request; + unexported.context.pixel = plain; + RecompileCacheKey::Build(unexported, key); + const auto plainKey = key; + plain.depthExportMin = 0.0f; + plain.depthExportMax = 1.0f; + unexported.context.pixel = plain; + RecompileCacheKey::Build(unexported, key); + require(key == plainKey, "depth export: the clamp keyed a shader without depth export"); + auto open = pixel; + open.depthExportMin = -std::numeric_limits::infinity(); + open.depthExportMax = std::numeric_limits::infinity(); + auto unclamped = request; + unclamped.context.pixel = open; + const auto unclampedResult = Recompile(unclamped); + const auto& openWords = unclampedResult.spirv.Words(); + bool openClamp = false; + bool openStore = false; + std::uint32_t openDepth = 0; + for (std::size_t at = 5; at < openWords.size() && (openWords[at] >> 16u) != 0; at += openWords[at] >> 16u) { + const auto op = static_cast(openWords[at] & 0xffffu); + if (op == spv::OpExtInst && openWords[at + 4] == 81u) openClamp = true; + if (op == spv::OpDecorate && openWords[at + 2] == spv::DecorationBuiltIn && openWords[at + 3] == spv::BuiltInFragDepth) openDepth = openWords[at + 1]; + if (op == spv::OpStore && openWords[at + 1] == openDepth && openDepth != 0u) openStore = true; + } + require(!openClamp && openStore, "depth export: an unclamped export (DISABLE_VIEWPORT_CLAMP) was clamped or not stored"); +} + void verifyVertexInfoVersion19() { using namespace ShaderRecompiler; const std::array code{0xbf810000u}; @@ -5427,7 +5507,7 @@ void verifyMergedHullConfiguration() { require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); - require(legacy.size() > 54u + pointTailBytes && legacy[4] == 44u, "a merged hull request did not use serialization version 44"); + require(legacy.size() > 54u + pointTailBytes && legacy[4] == 47u, "a merged hull request did not use serialization version 47"); legacy.resize(legacy.size() - 1u - pointTailBytes); legacy.pop_back(); require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); @@ -7848,6 +7928,7 @@ int main(int argc, char** argv) { verifyVertexInfoVersion19(); verifyDepthRemapRequests(); verifyMergedHullConfiguration(); + verifyDepthExportClamp(); verifyPositionExportComponents(); verifyLegacyPixelRequests(); verifyPixelExportReplay(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthExportClamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthExportClamp.cpp new file mode 100644 index 0000000000..19084d49af --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthExportClamp.cpp @@ -0,0 +1,75 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +alignas(4096) std::array Depth{}; + +alignas(256) constexpr std::array ExportOnePointFive{0x7e0002f2, 0xf800100f, 0x00000000, 0x7e0202ff, 0x3fc00000, 0xf8001881, 0x00000001, 0xbf810000}; +alignas(256) constexpr std::array ExportMinusHalf{0x7e0002f2, 0xf800100f, 0x00000000, 0x7e0202ff, 0xbf000000, 0xf8001881, 0x00000001, 0xbf810000}; + +Shaders WithDepthExport(AgcDriver::VulkanDevice& device, const Shaders& plain, std::span code, float minimum, float maximum) { + Shaders shaders = plain; + const auto target = device.Target(); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + 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); + pixel.depthExportEnable = true; + pixel.depthExportMin = minimum; + pixel.depthExportMax = maximum; + std::vector pixelUserData(4, 0u); + const std::array pixelMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderRecompiler::ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(plain.vertexPush, target) + }; + fragment.useCache = false; + shaders.pixel = ShaderRecompiler::Recompile(fragment); + return shaders; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto plain = Compile(*device); + const Surface surface{reinterpret_cast(Depth.data()), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + constexpr float low = 0.25f; + constexpr float high = 0.75f; + const auto high15 = WithDepthExport(*device, plain, ExportOnePointFive, low, high); + Require(Draw(*device, high15, surface, {.depthTest = true, .depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the depth-exporting draw did not cover the target"); + Require(Draw(*device, plain, surface, {.depthTest = true, .depthWrite = false, .depthCompare = VK_COMPARE_OP_GREATER, .minDepth = 0.9f, .maxDepth = 0.9f}) == Covered, "an exported depth of 1.5 was not clamped to ZMAX 0.75"); + const auto minus = WithDepthExport(*device, plain, ExportMinusHalf, low, high); + Require(Draw(*device, minus, surface, {.depthTest = true, .depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the second depth-exporting draw did not cover the target"); + Require(Draw(*device, plain, surface, {.depthTest = true, .depthWrite = false, .depthCompare = VK_COMPARE_OP_LESS, .minDepth = 0.1f, .maxDepth = 0.1f}) == Covered, "an exported depth of -0.5 was not clamped to ZMIN 0.25"); + const auto open = WithDepthExport(*device, plain, ExportOnePointFive, -std::numeric_limits::infinity(), std::numeric_limits::infinity()); + Require(Draw(*device, open, surface, {.depthTest = true, .depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the unclamped depth-exporting draw did not cover the target"); + Require(Draw(*device, plain, surface, {.depthTest = true, .depthWrite = false, .depthCompare = VK_COMPARE_OP_GREATER, .minDepth = 0.9f, .maxDepth = 0.9f}) == 0u, "an unclamped depth export (DISABLE_VIEWPORT_CLAMP) was clamped"); + std::puts("depth export clamp 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 fdcff33207..06f6516174 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -203,6 +203,10 @@ private: append(key, value.linearCentroid); append(key, value.pixelKillEnable); append(key, value.depthExportEnable); + if (value.depthExportEnable) { + append(key, std::bit_cast(value.depthExportMin)); + append(key, std::bit_cast(value.depthExportMax)); + } append(key, value.sampleMaskExportEnable); append(key, value.earlyZ); append(key, value.executeOnNoop); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 96bb58e1a5..db04971a1c 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -357,6 +357,10 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { for (const std::uint8_t location : info.sampleLocations) writer.WriteU8(location); writer.WriteBool(info.stencilOperationValueExport); for (const std::uint32_t order : info.sampleOrder) writer.WriteU32(order); + if (info.depthExportEnable) { + writer.WriteU32(std::bit_cast(info.depthExportMin)); + writer.WriteU32(std::bit_cast(info.depthExportMax)); + } } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -428,6 +432,10 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 42u) { for (std::uint32_t& order : info.sampleOrder) order = reader.ReadU32(); } + if (version >= 47u && info.depthExportEnable) { + info.depthExportMin = std::bit_cast(reader.ReadU32()); + info.depthExportMax = std::bit_cast(reader.ReadU32()); + } 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) | @@ -832,7 +840,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(44u); + writer.WriteU32(47u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -879,7 +887,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 > 44u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 47u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 3a9286f578..9278b1c2f7 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -184,6 +184,8 @@ struct ShaderPixelInputInfo { bool psNoPerspective = false; bool psPixelKillEnable = false; bool psDepthExportEnable = false; + float psDepthExportMin = 0.0f; + float psDepthExportMax = 1.0f; bool psSampleMaskExportEnable = false; bool psStencilRefExportEnable = false; bool psStencilOperationValueExport = false; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 1a90745c62..48e3b4fe33 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -170,6 +170,8 @@ struct ShaderPixelStageInfo { bool linearCentroid; bool pixelKillEnable; bool depthExportEnable; + float depthExportMin = 0.0f; + float depthExportMax = 1.0f; bool sampleMaskExportEnable; bool earlyZ; bool executeOnNoop; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 4f558e43a1..cd2eb50eb5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -4,6 +4,7 @@ #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvFlowEmitter.hpp" #include "SpirvBackend/SpirvBda.hpp" +#include #include #include #include "PipelineSpecialization.hpp" @@ -1920,7 +1921,14 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto raw = ExportRawComponent(ctx, data, 0); const auto f32 = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeF32(state), f32, raw); - state.module.AddFunction(spv::OpStore, state.depthVariable, f32); + const auto& pixel = *state.inputInfo.pixel; + if (std::isinf(pixel.psDepthExportMin) && std::isinf(pixel.psDepthExportMax)) { + state.module.AddFunction(spv::OpStore, state.depthVariable, f32); + } else { + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450NClamp, f32, ConstantF32Value(state, pixel.psDepthExportMin), ConstantF32Value(state, pixel.psDepthExportMax)); + state.module.AddFunction(spv::OpStore, state.depthVariable, clamped); + } } if ((exp.en & 2u) != 0u && state.stencilRefVariable != 0u) { auto raw = ExportRawComponent(ctx, data, 1); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index f1f7de5a92..e4db349e9c 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -151,6 +151,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psNoPerspective = pixel.noPerspective; pixelStorage.psPixelKillEnable = pixel.pixelKillEnable; pixelStorage.psDepthExportEnable = pixel.depthExportEnable; + pixelStorage.psDepthExportMin = pixel.depthExportMin; + pixelStorage.psDepthExportMax = pixel.depthExportMax; pixelStorage.psSampleMaskExportEnable = pixel.sampleMaskExportEnable; pixelStorage.psStencilRefExportEnable = pixel.stencilRefExportEnable; pixelStorage.psStencilOperationValueExport = pixel.stencilOperationValueExport; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index eb8d34ad6d..f14a68eba1 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -219,7 +219,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 44u; + upgraded[4] = 47u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); @@ -260,7 +260,7 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 44u; + layered[4] = 47u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); @@ -287,7 +287,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 44u; + bytes[4] = 47u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); @@ -418,7 +418,7 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false); auto upgraded = main38; - upgraded[4] = 44u; + upgraded[4] = 47u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); @@ -439,9 +439,9 @@ int main() { pointed.context.vertex->pointSize = 3.0f; pointed.context.vertex->pointSizeMin = 1.0f; pointed.context.vertex->pointSizeMax = 8.0f; - auto version44 = DecodeBase64(serializer.Serialize(pointed)); - Require(version44.size() > 15u && version44[4] == 44u); - auto version43 = version44; + auto version47 = DecodeBase64(serializer.Serialize(pointed)); + Require(version47.size() > 15u && version47[4] == 47u); + auto version43 = version47; version43.pop_back(); version43[4] = 43u; const auto replay43 = serializer.Deserialize(EncodeBase64(version43)); @@ -471,7 +471,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 44u; + main25[4] = 47u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -495,7 +495,7 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 44u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 47u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; version40.pop_back(); version40.pop_back(); @@ -503,7 +503,7 @@ int main() { const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 45u; + future[4] = 48u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -617,7 +617,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 44u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 15u : version >= 35u ? 16u : 21u)); + Require(upgradedLegacy[4] == 47u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 15u : version >= 35u ? 16u : 21u)); } } From 355a90b924ce9404a1481208ecd2e06da08fe52c Mon Sep 17 00:00:00 2001 From: Project-MOC <83237812+Project-MOC@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:07:00 +0200 Subject: [PATCH 017/702] test(relinker): cover Windows file writer behavior --- core/relinker/CMakeLists.txt | 15 +++ .../io/tests/WindowsFileWriterTests.cpp | 119 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 core/relinker/io/tests/WindowsFileWriterTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 5c4b89dfa8..f3dd86421a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -325,6 +325,21 @@ if(BUILD_TESTING) add_test(NAME param_json_utf8 COMMAND param_json_utf8_tests) set_tests_properties(param_json_utf8 PROPERTIES TIMEOUT 20) + if(WIN32) + add_test_executable(windows_file_writer_tests + ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/WindowsFileWriterTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ) + target_include_directories(windows_file_writer_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME windows_file_writer_binary COMMAND windows_file_writer_tests binary) + add_test(NAME windows_file_writer_text COMMAND windows_file_writer_tests text) + set_tests_properties(windows_file_writer_binary windows_file_writer_text PROPERTIES TIMEOUT 20) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(file_writer_tests ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileWriterTests.cpp diff --git a/core/relinker/io/tests/WindowsFileWriterTests.cpp b/core/relinker/io/tests/WindowsFileWriterTests.cpp new file mode 100644 index 0000000000..c42f909388 --- /dev/null +++ b/core/relinker/io/tests/WindowsFileWriterTests.cpp @@ -0,0 +1,119 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::string readFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + require(file.is_open(), "Cannot read test output"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +class ReadOnlyFile { + std::filesystem::path path; + DWORD attributes; + +public: + explicit ReadOnlyFile(const std::filesystem::path& file) : path(file), attributes(GetFileAttributesW(path.c_str())) { + require(attributes != INVALID_FILE_ATTRIBUTES, "Cannot read test file attributes"); + require(SetFileAttributesW(path.c_str(), attributes | FILE_ATTRIBUTE_READONLY) != 0, "Cannot protect test output"); + } + ~ReadOnlyFile() { SetFileAttributesW(path.c_str(), attributes); } + ReadOnlyFile(const ReadOnlyFile&) = delete; + ReadOnlyFile& operator=(const ReadOnlyFile&) = delete; +}; + +template +void requireOpenFailure(const std::filesystem::path& path, const TValue& data) { + const auto name = Io::Utf8Path(path); + try { + Io::FileWriter().Write(name, data); + } catch (const Domain::RelinkerException& error) { + require(error.what() == "Cannot open output file: " + name, "Unexpected output error: " + std::string(error.what())); + return; + } + throw std::runtime_error("Writing an unavailable output reported success"); +} + +template +void run(const std::filesystem::path& directory, const TValue& data, const std::string& expected) { + Io::FileWriter writer; + const auto output = directory / std::filesystem::path(u8"Gr\u00fc\u00dfe-\u65e5\u672c.bin"); + const auto name = Io::Utf8Path(output); + writer.Write(name, data); + require(readFile(output) == expected, "A new output has incorrect bytes"); + + const std::string previous(4096, 'x'); + std::ofstream(output, std::ios::binary) << previous; + writer.Write(name, data); + require(readFile(output) == expected, "Replacing an output left old bytes or changed the data"); + writer.Write(name, TValue{}); + require(readFile(output).empty(), "An empty output retained old bytes"); + + std::ofstream(output, std::ios::binary) << previous; + { + const auto raw = CreateFileW(output.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + require(raw != INVALID_HANDLE_VALUE, "Cannot lock test output"); + const std::unique_ptr locked(raw, CloseHandle); + requireOpenFailure(output, data); + require(readFile(output) == previous, "A sharing violation changed the previous output"); + } + { + const ReadOnlyFile readOnly(output); + requireOpenFailure(output, data); + require(readFile(output) == previous, "A read-only output was changed"); + } + writer.Write(name, data); + require(readFile(output) == expected, "Writing after releasing output protection failed"); + + const auto missing = directory / "missing" / "output.bin"; + requireOpenFailure(missing, data); + require(!std::filesystem::exists(missing.parent_path()), "A failed write created a parent directory"); + requireOpenFailure(directory, data); + require(std::filesystem::is_directory(directory), "A failed write replaced a directory"); +} + +} + +int main(int argc, char** argv) { + const auto directory = std::filesystem::temp_directory_path() / + ("anyps5-windows-file-writer-" + std::to_string(GetCurrentProcessId()) + "-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())) / + std::filesystem::path(u8"\u00dcbertragung-\u65e5\u672c"); + try { + require(argc == 2, "Expected binary or text"); + std::filesystem::create_directories(directory); + const std::string mode(argv[1]); + if (mode == "binary") { + const std::vector data{0x00, 0x0a, 0x0d, 0x1a, 0x80, 0xff}; + run(directory, data, std::string(data.begin(), data.end())); + } else if (mode == "text") { + const std::string data("first\n\0last\n", 12); + run(directory, data, std::string("first\r\n\0last\r\n", 14)); + } else { + throw std::runtime_error("Expected binary or text"); + } + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + std::error_code ignored; + std::filesystem::remove_all(directory.parent_path(), ignored); + return 1; + } + std::filesystem::remove_all(directory.parent_path()); + return 0; +} From 24337dc1c883b89a0a002939ec45066f6d1078a9 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:23:50 +0000 Subject: [PATCH 018/702] fix(shader): plan an image table for a T# read at a first-lane offset A compute shader that reads a T# with s_buffer_load_dwordx8 at an offset taken from v_readfirstlane_b32, without a waterfall loop, stopped with "GetImageResource dword 0 is not a valid runtime value": the table column only accepted an offset of the form key << n, key * n or a masked one, and here the lane value is scaled before the read (v_lshlrev_b32 by 5, then v_lshl_add_u32 x, 1, x for a 96-byte record) and merged across divergent branches. PPSA26344 compute shaders 0x80001f1100 and 0x8000139c00 stop on this. Accept a first-lane offset whose value is a known multiple of a dword stride as an unkeyed table column with that stride. The multiple is found through shifts, multiplies by immediates, sums of one scaled value, selects, Phis and selects the read's active mask implies. AI-assisted. Tested by me on my own machine. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 35 ++++++ .../Optimization/src/ResourceTracker.cpp | 111 +++++++++++++++++- 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index e206810771..f4f111df0c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2856,6 +2856,40 @@ void verifyImageTableMaskedOffset() { require(codes.size() == 4u && codes[0] == codes[3] && codes[2] == Abi::NullCode && codes[1] != codes[0] && (codes[1] & Abi::PoisonFlag) == 0u, "image table masked offset: the whole table map is wrong"); } +void verifyImageTableFirstLaneScale() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + std::fill(heap.begin(), heap.end(), std::array{}); + heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[3] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + heap[9] = heap[0]; + FillTableSrt(guest, heap.data(), 32u, 12u); + const auto userData = TableUserData(guest); + const auto scaled = [](std::initializer_list offset) { + std::vector code(TableWholeCode); + const auto first = std::find(code.begin(), code.end(), 0x7e200500u); + require(first != code.end() && first[1] == 0x8f108510u, "image table first-lane scale: the program has no v_readfirstlane_b32 and s_lshl_b32"); + code.insert(code.erase(first, first + 2), offset); + return code; + }; + const auto code = scaled({0x34020085u, 0xd7460001u, 0x04050301u, 0x7e200501u}); + const auto request = TableRequest(code, userData); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.images[0].tableOperation == TableOperation::Supported, "image table first-lane scale: the T# was not planned as a table"); + const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.has_value() && column->stride == 96u && column->addend == 0u && column->offset == 0u && column->dwordCount == 8u && column->unkeyed, "image table first-lane scale: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + require(snapshot.records == 4u && snapshot.keys == 4u && snapshot.fault == 0u, "image table first-lane scale: the snapshot does not hold the four 96-byte records"); + const auto codes = TableCodes(TableMap(*compiled.result), 0); + require(codes.size() == 4u && codes[0] == codes[3] && codes[2] == Abi::NullCode && codes[1] != codes[0] && (codes[0] & Abi::PoisonFlag) == 0u && (codes[1] & Abi::PoisonFlag) == 0u, "image table first-lane scale: the table map is wrong"); + const auto unscaled = scaled({0x7e200500u}); + expectFailure([&] { static_cast(GetResourcePlan(TableRequest(unscaled, userData))); }, "GetImageResource dword 0 is not a valid runtime value", "image table first-lane scale: a first-lane offset with no dword multiple was accepted"); +} + void verifyImageTableRegistration() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -7752,6 +7786,7 @@ int main(int argc, char** argv) { verifyImageTableBulkKeyScan(); verifyImageTableSrtKey(); verifyImageTableMaskedOffset(); + verifyImageTableFirstLaneScale(); verifyImageTableKeyUnion(); verifyImageTableLateKey(); verifyImageTableUnkeyedAccess(); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 0ffeb3be74..84b6b6e92c 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -516,6 +517,113 @@ private: return MatchScale(scaled, key, stride) && std::has_single_bit(stride) && stride >= 4u && (mask & ~(stride - 1u)) != 0u && key->Type() == IrType::U32; } + static IrValue* ActiveLaneValue(IrValue* value, const IrValue* mask) { + value = value->Resolve(); + while (mask != nullptr && Detail::IsRuntimeSelect(value->Opcode()) && value->ArgumentCount() == 3u && Detail::ActiveMaskImplies(mask, value->Argument(0))) { + value = value->Argument(1)->Resolve(); + } + return value; + } + + static std::uint32_t ScaledBase(IrValue* value, const IrValue* mask, IrValue*& base) { + value = ActiveLaneValue(value, mask); + std::uint32_t immediate = 0; + std::uint32_t scale = 1u; + while (value->ArgumentCount() == 2u && immediateU32(value->Argument(1), immediate) && ((value->Opcode() == IrOpcode::ShiftLeftLogical32 && immediate < 32u) || value->Opcode() == IrOpcode::IMul32)) { + const std::uint64_t next = static_cast(scale) * (value->Opcode() == IrOpcode::IMul32 ? immediate : 1u << immediate); + if (next > std::numeric_limits::max()) { + break; + } + scale = static_cast(next); + value = ActiveLaneValue(value->Argument(0), mask); + } + base = value; + return scale; + } + + struct MultipleSearch { + std::vector visiting; + std::unordered_map known; + const IrValue* mask = nullptr; + bool cycle = false; + }; + + std::uint32_t KnownMultiple(IrValue* value, MultipleSearch& search, std::uint32_t depth) const { + value = ActiveLaneValue(value, search.mask); + std::uint32_t immediate = 0; + if (immediateU32(value, immediate)) { + return immediate; + } + if (std::ranges::find(search.visiting, value) != search.visiting.end()) { + search.cycle = true; + return 0u; + } + if (const auto found = search.known.find(value); found != search.known.end()) { + return found->second; + } + if (depth == 0u || value->Type() != IrType::U32) { + return 1u; + } + const bool outerCycle = search.cycle; + search.cycle = false; + auto& visiting = search.visiting; + visiting.push_back(value); + const auto product = [](std::uint32_t multiple, std::uint32_t scale) { + const std::uint64_t result = static_cast(multiple) * scale; + return result > std::numeric_limits::max() ? std::uint32_t{1u} << std::countr_zero(scale) : static_cast(result); + }; + std::uint32_t multiple = 1u; + const auto op = value->Opcode(); + if (value->IsPhi()) { + multiple = 0u; + for (std::size_t index = 0; index < value->ArgumentCount(); index++) { + multiple = std::gcd(multiple, KnownMultiple(value->Argument(index), search, depth - 1u)); + } + } else if ((op == IrOpcode::ShiftLeftLogical32 || op == IrOpcode::IMul32) && value->ArgumentCount() == 2u && immediateU32(value->Argument(1), immediate)) { + IrValue* base = nullptr; + const auto scale = ScaledBase(value, search.mask, base); + multiple = product(KnownMultiple(base, search, depth - 1u), scale); + } else if (op == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { + IrValue* left = nullptr; + IrValue* right = nullptr; + const auto leftScale = ScaledBase(value->Argument(0), search.mask, left); + const auto rightScale = ScaledBase(value->Argument(1), search.mask, right); + const std::uint64_t sum = static_cast(leftScale) + rightScale; + if (EquivalentValue(m_program.Resources(), left, right) && sum <= std::numeric_limits::max()) { + multiple = product(KnownMultiple(left, search, depth - 1u), static_cast(sum)); + } else { + multiple = std::gcd(KnownMultiple(value->Argument(0), search, depth - 1u), KnownMultiple(value->Argument(1), search, depth - 1u)); + } + } else if (Detail::IsRuntimeSelect(op) && value->ArgumentCount() == 3u) { + multiple = std::gcd(KnownMultiple(value->Argument(1), search, depth - 1u), KnownMultiple(value->Argument(2), search, depth - 1u)); + } else if (op == IrOpcode::ReadFirstLane && value->ArgumentCount() == 2u && search.mask == nullptr) { + search.mask = value->Argument(1)->Resolve(); + multiple = KnownMultiple(value->Argument(0), search, depth - 1u); + search.mask = nullptr; + } + visiting.pop_back(); + if (!search.cycle) { + search.known.emplace(value, multiple); + } + search.cycle = search.cycle || outerCycle; + return multiple; + } + + bool MatchFirstLaneOffset(IrValue* value, IrValue*& key, std::uint32_t& stride) const { + value = value->Resolve(); + if (value->Opcode() != IrOpcode::ReadFirstLane || value->ArgumentCount() != 2u || value->Type() != IrType::U32) { + return false; + } + MultipleSearch search; + const auto multiple = KnownMultiple(value, search, firstLaneMultipleDepth); + if (multiple < 4u || multiple % 4u != 0u) { + return false; + } + key = value; + stride = multiple; + return true; + } + static bool ReadsThroughDescriptor(const IrValue* value) { std::vector pending{value}; std::vector seen; @@ -700,7 +808,7 @@ private: return TryMakeIndirectColumn(handle, *heapHandle, sampler, words, descriptorOffset, first, plan); } if (!MatchAffineOffset(offset, key, stride, addend)) { - if (!MatchMaskedOffset(offset, key, stride)) { + if (!MatchMaskedOffset(offset, key, stride) && (address || !MatchFirstLaneOffset(offset, key, stride))) { return false; } masked = true; @@ -863,6 +971,7 @@ private: } static constexpr std::uint32_t phiSearchDepth = 8u; + static constexpr std::uint32_t firstLaneMultipleDepth = 32u; static bool SplittableImageRead(const IrValue& inst) { switch (inst.Opcode()) { From d7bb63bb3b951df45294c34cc0c63028577736f9 Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Wed, 21 Oct 2026 09:21:58 +0200 Subject: [PATCH 019/702] perf(shader): skip log2 correction outside the near-one range Evaluate the existing accuracy correction only inside its established input range. Keep the native log2 result and all correction thresholds and arithmetic unchanged, and cover inputs adjacent to both range boundaries in the existing GPU test. --- .../tests/execution/LogNearOne.cpp | 21 +++++++++++++++++-- .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 14 +++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LogNearOne.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LogNearOne.cpp index 8821426321..49af2cd503 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LogNearOne.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LogNearOne.cpp @@ -193,8 +193,8 @@ bool IsPinned(std::uint32_t bits, std::uint32_t width) { return magnitude == 0u || magnitude >= infinity; } -void Run(AgcDriver::VulkanDevice& device) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); +void Run(AgcDriver::VulkanDevice& device, const std::uint32_t (&rows)[Threads][Inputs] = Rows) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(rows[tid]), std::end(rows[tid]), &Input[tid * Inputs]); Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); @@ -235,6 +235,22 @@ void Check() { } } +void CheckBoundaries(AgcDriver::VulkanDevice& device) { + constexpr std::uint32_t cases[][3] = { + {0x3f5fffffu, 0xbe4544c7u, 12u}, + {0x3f600001u, 0xbe4544bau, 6u}, + {0x3f8fffffu, 0x3e2e00c8u, 6u}, + {0x3f900001u, 0x3e2e00dcu, 12u} + }; + std::uint32_t rows[Threads][Inputs]{}; + for (std::size_t lane = 0; lane < std::size(cases); ++lane) rows[lane][0] = cases[lane][0]; + Run(device, rows); + for (std::size_t lane = 0; lane < std::size(cases); ++lane) { + const auto actual = Output[lane * Results]; + Require(Distance(actual, cases[lane][1], 32u) <= cases[lane][2], "log near-one boundary: input " + Hex(cases[lane][0]) + " is " + Hex(actual) + ", expected " + Hex(cases[lane][1])); + } +} + } int main() { @@ -243,6 +259,7 @@ int main() { if (!device) return VulkanTestSkipped; Run(*device); Check(); + CheckBoundaries(*device); std::puts("log near one tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 1ce65a7506..69d77f4842 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -1015,13 +1015,15 @@ std::uint32_t Log2SeriesNearOne(SpirvEmitterState& state, std::uint32_t differen std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { const auto value = EmitExt(state, TypeF32(state), GLSLstd450Log2, {arg0}); const auto difference = ExactSub(state, arg0, ConstantF32(state, 0x3f800000u)); - const auto series = Log2SeriesNearOne(state, difference); - 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)}); const auto near = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), EmitExt(state, TypeF32(state), GLSLstd450FAbs, {difference}), ConstantF32(state, 0x3e000000u)); - const auto far = Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)); - return Select(state, TypeF32(state), Binary(state, spv::OpLogicalAnd, TypeBool(state), near, far), series, value); + return EmitValueOrDefaultIfCondition(state, near, TypeF32(state), value, [&] { + const auto series = Log2SeriesNearOne(state, difference); + 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)}); + const auto far = Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)); + return Select(state, TypeF32(state), far, series, value); + }); } std::uint32_t SinTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { From 33aaab29be4330ab6f0aa2309a94b97f540bac1c Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Wed, 21 Oct 2026 13:05:28 +0200 Subject: [PATCH 020/702] perf(shader): use 32-bit scalar buffer bounds --- .../execution/BufferDescriptorRuntime.cpp | 25 +++++++++++++++++++ .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 15 ++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index 51fa4f8630..0a1624612e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Recompiler.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "Optimization/ResourceProgram.hpp" #include "VulkanTestDevice.hpp" #include @@ -150,6 +151,30 @@ void ScalarReads(AgcDriver::VulkanDevice& device) { device.WaitIdle(); Require(Output == baseline, "vector or mixed buffer read changed with reserved bit 30 set"); } + request.shader.code = scalarCode; + request.shader.codeAddress = reinterpret_cast(scalarCode.data()); + struct BoundsCase { + std::uint32_t stride; + std::uint32_t records; + bool inBounds; + }; + constexpr std::array bounds{{ + {0u, 0u, false}, {0u, 7u, false}, {0u, 8u, true}, {1u, 7u, false}, + {2u, 3u, false}, {2u, 4u, true}, {4u, 0x40000000u, true}, {0x3fffu, 0xffffffffu, true}, + }}; + GuestAllocations::Mutation().Add(Input.data(), Input.size(), true, false, true); + for (const auto& test : bounds) { + auto input = BufferDescriptor(Input.data(), test.records); + input[1] |= test.stride << 16u; + std::copy(input.begin(), input.end(), userData.begin()); + Output.fill(Fill); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1); + device.WaitIdle(); + const auto value = test.inBounds ? expected : 0u; + Require(std::all_of(Output.begin(), Output.end(), [value](std::uint32_t word) { return word == value; }), "scalar buffer bounds differ for stride " + std::to_string(test.stride) + " records " + std::to_string(test.records)); + } + GuestAllocations::Mutation().Remove(Input.data()); } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index f3736dc698..078f21e2c7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -849,14 +849,21 @@ std::uint32_t EmitRuntimeScalarBufferLoad(SpirvValueEmitContext& context, const if (std::ranges::find(state.supportedCapabilities, spv::CapabilityInt64) == state.supportedCapabilities.end()) context.Fail(instruction, "scalar buffer bounds require shaderInt64"); state.module.EmitCapability(spv::CapabilityInt64); const auto resource = descriptor(context, instruction); - const auto u64 = TypeScalarU64(state); - const auto size = Select(state, u64, equal(state, resource.stride, ConstantU32(state, 0u)), Unary(state, spv::OpUConvert, u64, resource.records), Binary(state, spv::OpIMul, u64, Unary(state, spv::OpUConvert, u64, resource.stride), Unary(state, spv::OpUConvert, u64, resource.records))); + const auto u32 = TypeU32(state); + const auto stride = Select(state, u32, equal(state, resource.stride, ConstantU32(state, 0u)), ConstantU32(state, 1u), resource.stride); + const auto size = state.module.AllocateId(); + const auto low = state.module.AllocateId(); + const auto high = state.module.AllocateId(); + state.module.AddFunction(spv::OpUMulExtended, state.module.Type(spv::OpTypeStruct, u32, u32), size, stride, resource.records); + state.module.AddFunction(spv::OpCompositeExtract, u32, low, size, 0u); + state.module.AddFunction(spv::OpCompositeExtract, u32, high, size, 1u); const auto address = EmitAddU32(state, context.Arg(instruction, 1u), ConstantU32(state, context.Memory(instruction).offset)); const auto noCarry = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), address, context.Arg(instruction, 1u)); const auto byte = EmitAndConstant(state, address, ~3u); - const auto offset = Unary(state, spv::OpUConvert, u64, byte); - const auto inBounds = both(state, noCarry, both(state, resource.valid, Binary(state, spv::OpULessThanEqual, TypeBool(state), Binary(state, spv::OpIAdd, u64, offset, BdaConstant(state, 4u)), size))); + const auto fits = Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, high), Binary(state, spv::OpULessThan, TypeBool(state), byte, EmitAndConstant(state, low, ~3u))); + const auto inBounds = both(state, noCarry, both(state, resource.valid, fits)); if (!context.Memory(instruction).gpuDescriptor) return readBuffer(context, instruction, byte, 32u, inBounds); + const auto offset = Unary(state, spv::OpUConvert, TypeScalarU64(state), byte); return EmitValueOrZeroIfCondition(state, inBounds, [&] { return readBuffer(context, instruction, AddBdaAddress(context, instruction, resource.base, offset, false), 32u); }); } From cfe3c78b8cca85c9b595c6fe8db900e9a88849d6 Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 19:37:34 -0300 Subject: [PATCH 021/702] fix(agc): eliminate a fast clear over part of a CMASK color target Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 6 ++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index a730b9df79..88d6f5e8bb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -2161,7 +2161,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); } Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); - requireCovered(target); + if (pass.mode != ColorMetadataPass::Mode::EliminateFastClear || target.dccAddress != 0) requireCovered(target); pass.targets.push_back(target); } return pass; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 3c04279773..c2b62f2159 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3085,6 +3085,9 @@ void metadataPassTests() { queue.context[0x91] = 0x40040; queue.context[0x10f] = std::bit_cast(16.0f); expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target"); + queue.context[0x202] = 0xcc0020; + Require(DecodeColorMetadataPass(queue).has_value(), "a fast-clear eliminate of a target without DCC keys over part of it was refused"); + queue.context[0x202] = 0xcc0060; queue.context[0x10f] = std::bit_cast(32.0f); queue.context[0x202] = 0xcc0030; queue.context[0x1c5] = 9; @@ -3103,6 +3106,9 @@ void metadataPassTests() { queue.context[0x324] = 0; pass = DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->targets.size() == 1 && pass->targets[0].dccAddress == keysAddress, "the metadata pass lost the target's DCC keys"); + auto partial = queue; + partial.context[0x10f] = std::bit_cast(16.0f); + expectFailure([&] { DecodeColorMetadataPass(partial); }, "over part of a color target"); auto mipmapped = queue; mipmapped.context[0x3b0] |= 1u << 28u; expectFailure([&] { DecodeColorMetadataPass(mipmapped); }, "mipmapped DCC"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 75f3df451c..aed85c9e7f 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -173,6 +173,7 @@ - [Sample 0 of a multisampled color target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp) (libSceAgcDriver) - a 2D image read of a multisampled color target returns its float, UNORM, SNORM or sRGB sample 0 exactly only where the driver keeps those values through a shader and a color attachment, which Vulkan does not require, and one of a multisampled depth surface throws. The read binds a one-sample copy that a fragment pass (`texelFetch` of sample 0) draws before every such binding; integer formats copy exactly. - [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 and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. 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, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. Ordinary sampled and storage bindings check the color-target guard before resource reuse, including precollected sampled lookups. Reused image tables do not invalidate on multisample color-target registration yet; their per-element fault handling and native multisample sampling require a separate reuse proof. +- [CB fast-clear eliminate over part of a target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `CB_COLOR_CONTROL` mode 2 pass whose viewport or scissor covers only part of a CMASK color target without DCC keys is accepted and eliminates the fast clear of the whole target, as a full pass does. Every tile CMASK marks as cleared already stands for the clear colour, so the texels match, but the tiles outside the drawn area end expanded in CMASK where the hardware leaves them cleared: a title that reads those CMASK entries, or changes the clear colour before eliminating the rest, would see the difference. DEATHLOOP (PPSA01670) issues such a pass over 3688x2112 of a 3840x2160 target for its dynamic resolution. A partial pass over DCC keys, and the other modes, still throw - [DB depth copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `DB_RENDER_CONTROL.DEPTH_COPY` is run as a compute pass that copies one sample of the `Z_32_FLOAT` depth surface into a single-sample R32 float color target over the whole surface. With `COPY_CENTROID` the sample is the first one the draw covers, which is sample 0 under the full `PA_SC_AA_MASK` the driver requires: [radeonsi copies one sample per draw with `COPY_CENTROID` and a one-sample mask](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1679-1681); the console is not measured. Stencil copies, depth tests, other depth formats, multisampled or other-format targets and copies over part of the surface throw. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. PA_SC_AA_CONFIG is only compared with CB0 while PA_SC_MODE_CNTL_0.MSAA_ENABLE is set: PPSA08270 resolves a 4x 512x512 target with MSAA_ENABLE off and PA_SC_AA_CONFIG still at 2x (0x00108001) from earlier draws. - [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 use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each count, per count and for every count; an RTX 3070 Laptop GPU (driver 617.42) reports `maxSampleLocationGridSize` 1x1 for every count (`VkPhysicalDeviceSampleLocationsPropertiesEXT`), but per count (`VkMultisamplePropertiesEXT`, which the device setup reads and logs) a 2x4 grid at 2 samples, 2x2 at 4 and 1x2 at 8. So the 3070's 2x patterns, the 2x checkerboard among them, take the native grid, and the stand-in plus the exact path serve its 8-sample patterns and devices without a grid. A pattern that only reorders one set of positions between pixels, as a 2x checkerboard pattern puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, is drawn exactly through that stand-in: before every render pass that draws with it each pixel's samples are put in the stand-in's order and after it back in the guest's, through a storage view of a multisampled color target and through a copy and a per-sample pass for depth (writing stencil through VK_EXT_shader_stencil_export, or one pass per bit without it), while the pixel shader reads SampleID, applies the sample mask and exports coverage in the guest's order, and the second log line names the reordering. A uniform partial sample mask that the order moves is applied in the pixel shader. With early Z, a depth or stencil test and memory writes in the pixel program, the draw throws, where #6451's stand-in alone ran it with odd rows' sample indices swapped. A fix would choose the stand-in after compilation. That costs two passes over each multisampled attachment per render pass, and on those devices every multisampled color target is created with a mutable format and storage usage for its view, which a driver may answer by not compressing it. It needs shaderStorageImageMultisample with a storage view of the color format at the sample count, sampleRateShading, and a depth format a FragDepth write gives back exactly (16-bit, or 32-bit float; not 24-bit depth, the stand-in for 16-bit depth with stencil where the device has neither); without them, and for patterns whose pixels hold different positions, the stand-in alone is used, and the log line says why. A device without shaderStorageImageMultisample takes the stand-in alone for every pass, depth-only passes included, so a depth target that a depth-only pass and a color pass both draw keeps one order. On a device that has it, a pass is still refused for a color format without a storage view at its sample count, while a depth-only pass on the same depth target is reordered, so that target's samples change order between those passes; a fix would decide the order per target. Then the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A same-set pattern keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values, but sample i of an odd row holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. The pixel shader runs once per pixel with sample number 0, so ANCILLARY reads sample 0 and POS_X_FLOAT and POS_Y_FLOAT keep the position `SPI_BARYC_CNTL` names. PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts 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. A pass that draws depth at custom sample locations ends with a transition to TRANSFER_SRC_OPTIMAL and back that names them. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. From a7e1630192216862292bac7c234f7f95d97d07c1 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sat, 24 Oct 2026 13:47:48 +1300 Subject: [PATCH 022/702] test(agc): give the stencil replay pixel info the depth clamp registers A pixel shader that exports depth now reads DB_RENDER_OVERRIDE and the viewport depth bounds (PA_SC_VPORT_ZMIN_0 and ZMAX_0) while its stage info is decoded. The stencil replay test builds its context registers by hand and left them out, so the decode threw "missing register at DWORD 0x3" for its depth-exporting shader. The test now sets the override off and the bounds to 0 and 1, which is what the decode produced before the clamp. --- core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp index 3612fae6ef..4420abd95d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp @@ -83,7 +83,7 @@ std::array BufferDescriptor(const void* data, std::uint32_t st ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { const AgcDriver::Registers context{ - {0x1b3, 0x700u}, {0x1b4, 0x700u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, shaderControl}, + {0x003, 0u}, {0x0b4, 0u}, {0x0b5, 0x3f800000u}, {0x1b3, 0x700u}, {0x1b4, 0x700u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, shaderControl}, }; return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); } From fa02afeb4795b8d99bea24e054208db003fa2f2c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:07:40 +0200 Subject: [PATCH 023/702] fix(agc): bound the draw cache by bytes --- .../include/Driver/Draw/DrawCache.hpp | 2 +- .../Execution/include/Driver/Driver.hpp | 3 + .../Execution/include/ShaderMemory.hpp | 1 + .../src/Driver/Dispatch/DispatchCache.cpp | 3 +- .../Execution/src/Driver/Draw/DrawCache.cpp | 16 ++- .../src/Driver/Draw/DrawCacheStatistics.cpp | 9 +- .../Execution/src/ShaderMemory.cpp | 4 + .../tests/execution/ImageTableCacheBounds.cpp | 102 ++++++++++++++++++ docs/dev/ENVIRONMENT.md | 1 + 9 files changed, 136 insertions(+), 5 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 d3f3da21fa..7ae26d17b2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -98,7 +98,7 @@ struct DrawEntryCounters { std::uint64_t registerKeyLookups = 0, registerKeyHits = 0, decodeSkipped = 0, decodePartial = 0, facadeMismatches = 0, verifyDecodes = 0, verifyDecodeMismatches = 0; double keyUs = 0; - std::uint64_t absentNewRegisters = 0, absentUserWords = 0, absentEvicted = 0, absentNeverInserted = 0, evictions = 0; + std::uint64_t absentNewRegisters = 0, absentUserWords = 0, absentEvicted = 0, absentNeverInserted = 0, evictions = 0, byteEvictions = 0; std::array registerKeyMissChanges{}; std::uint64_t differingSameRuns = 0, differingWords = 0, differingRunsChanged = 0; std::uint64_t dataHits = 0, dataStagesReused = 0, dataStagesCompiled = 0, dataVerified = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 922c68099c..23a3557f9a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -96,7 +96,9 @@ public: std::uint64_t variantBytes = 0; }; DispatchCacheFootprint DispatchCacheSize(); + DispatchCacheFootprint DrawCacheSize(); static std::uint64_t DispatchCacheBytes(); + static std::uint64_t DrawCacheBytes(); static ShaderMemory::PendingWrite QueryPendingSpan(std::uint64_t address, std::size_t bytes, std::span known); std::shared_ptr RegisteredShaders(); @@ -408,6 +410,7 @@ private: std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0; std::optional drawKeySightings; std::optional drawShapeSightings; + std::uint64_t drawCacheByteEvictions = 0; DrawEntryCounters drawEntryCounters; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp index dacb784275..8420887e1b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp @@ -81,6 +81,7 @@ public: }; static SupplyCounts Supplies(); [[nodiscard]] std::vector> SuppliedBlocks() const; + [[nodiscard]] std::size_t Bytes() const; struct VouchCounts { std::uint64_t calls, blocks, refused, words; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp index 732d7733b4..a691951f22 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp @@ -65,7 +65,8 @@ Driver::DispatchCacheFootprint Driver::DispatchCacheSize() { } std::uint64_t Driver::variantBytes(const DispatchVariant& variant) { - return sizeof(DispatchVariant) + variant.words.size() * sizeof(std::uint32_t) + variant.shared.size() * sizeof(SharedWords) + variant.runs.size() * sizeof(std::pair) + variant.captured.size() * sizeof(ShaderRecompiler::MemoryRegion) + (variant.vertexInfo != nullptr ? sizeof(ShaderRecompiler::ShaderVertexStageInfo) : 0); + const auto held = [](const auto& values) { return static_cast(values.capacity() * sizeof(values[0])); }; + return sizeof(DispatchVariant) + held(variant.runs) + held(variant.words) + held(variant.shared) + held(variant.dataPositions) + held(variant.dataSlots) + held(variant.runBases) + held(variant.shaderDataPatches) + held(variant.captured) + held(variant.spans) + (variant.vertexInfo != nullptr ? sizeof(ShaderRecompiler::ShaderVertexStageInfo) : 0) + (variant.memory != nullptr ? variant.memory->Bytes() : 0); } void Driver::accountVariant(const DispatchVariant& variant, bool added) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index 8d05f4693f..c42ca7d2e4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -65,6 +65,15 @@ std::size_t Driver::drawCacheEntries() { return entries; } +std::uint64_t Driver::DrawCacheBytes() { + static const std::uint64_t bytes = [] { + const char* text = std::getenv("APS5_DRAW_CACHE_BYTES"); + const auto parsed = text != nullptr ? std::strtoull(text, nullptr, 10) : 0ull; + return parsed != 0 ? static_cast(parsed) : std::uint64_t{256} << 20u; + }(); + return bytes; +} + void Driver::accountDrawVariant(const DispatchVariant& variant, bool added) { if (added) { ++drawCacheVariants; @@ -180,7 +189,8 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vectororder); found->second = std::move(replacement); } - while (drawCache.size() > drawCacheEntries()) { + while (drawCache.size() > drawCacheEntries() || (drawCache.size() > 1 && drawCacheVariantBytes > DrawCacheBytes())) { + const bool overBytes = drawCache.size() <= drawCacheEntries(); const auto last = drawCache.find(drawOrder.back()); for (const auto& variants : last->second->stages) { for (const auto& variant : variants) accountDrawVariant(*variant, false); @@ -191,6 +201,10 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vector(counters.hits) / static_cast(counters.lookups) : 0.0, count(misses), count(counters.absent), count(counters.dataHits), count(counters.dataStagesReused), count(counters.dataStagesCompiled), count(counters.dataVerified), count(counters.absentNewRegisters), count(counters.absentUserWords), count(counters.absentEvicted), count(counters.absentNeverInserted), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.differingSameRuns), counters.differingSameRuns != 0 ? static_cast(counters.differingWords) / static_cast(counters.differingSameRuns) : 0.0, count(counters.differingRunsChanged), drawCache.size(), count(counters.evictions)); + std::snprintf(line, sizeof(line), "[draw-cache] 10 s: %llu lookups, %llu hits (%.1f%%), %llu stage misses, %llu without an entry, %llu data-only hits (stages: %llu reused, %llu compiled; %llu verified); no entry: new registers %llu, user words/pointers only %llu, evicted %llu, seen but never inserted %llu; stage misses: front differing %llu, fragment differing %llu, other differing %llu, layout %llu, gate %llu, stage count %llu; differing captures: same runs %llu (%.1f words changed each), other runs %llu; %zu entries, %llu evicted (%llu over the byte budget)\n", count(counters.lookups), count(counters.hits), counters.lookups != 0 ? 100.0 * static_cast(counters.hits) / static_cast(counters.lookups) : 0.0, count(misses), count(counters.absent), count(counters.dataHits), count(counters.dataStagesReused), count(counters.dataStagesCompiled), count(counters.dataVerified), count(counters.absentNewRegisters), count(counters.absentUserWords), count(counters.absentEvicted), count(counters.absentNeverInserted), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.differingSameRuns), counters.differingSameRuns != 0 ? static_cast(counters.differingWords) / static_cast(counters.differingSameRuns) : 0.0, count(counters.differingRunsChanged), drawCache.size(), count(counters.evictions), count(counters.byteEvictions)); text += line; std::snprintf(line, sizeof(line), "[draw-cache] register-key miss component changes (10 s): stable registers %llu, user data %llu, auxiliary registers %llu, device %llu, first shader set %llu, unchanged components %llu\n", count(counters.registerKeyMissChanges[0]), count(counters.registerKeyMissChanges[1]), count(counters.registerKeyMissChanges[2]), count(counters.registerKeyMissChanges[3]), count(counters.registerKeyMissChanges[4]), count(counters.registerKeyMissChanges[5])); text += line; @@ -46,6 +46,11 @@ DrawEntryCounters Driver::DrawCacheCounters() { return drawEntryCounters; } +Driver::DispatchCacheFootprint Driver::DrawCacheSize() { + std::lock_guard cacheLock(drawCacheMutex); + return {drawCache.size(), drawCacheVariantBytes}; +} + std::string Driver::profileDrawCacheLines(const DrawEntryCounters& counters) { std::string text; const auto append = [&](const char* format, auto... values) { @@ -62,7 +67,7 @@ std::string Driver::profileDrawCacheLines(const DrawEntryCounters& counters) { std::snprintf(item, sizeof(item), "%s%llu", rank == 0 ? "" : " / ", count(counters.variantHitsByRank[rank])); ranks += item; } - append("[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu shape updates, %llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB), %llu evictions in total, %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast(counters.variantsCompared) / static_cast(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.shapeUpdates), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast(drawCacheVariantBytes) / (1024.0 * 1024.0), count(drawCacheEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches)); + append("[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu shape updates, %llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB of a %.1f MiB budget), %llu evictions in total (%llu over the byte budget), %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast(counters.variantsCompared) / static_cast(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.shapeUpdates), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast(drawCacheVariantBytes) / (1024.0 * 1024.0), static_cast(DrawCacheBytes()) / (1024.0 * 1024.0), count(drawCacheEvictions), count(drawCacheByteEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches)); append("[draw-cache] register key (10 s): %llu lookups, %llu hits, key build %.1f us each; decode skipped %llu, partial (state/pixel/programs from the entry) %llu; facade log != table %llu; verify: %llu decodes compared, %llu differed\n", count(counters.registerKeyLookups), count(counters.registerKeyHits), counters.registerKeyLookups != 0 ? counters.keyUs / static_cast(counters.registerKeyLookups) : 0.0, count(counters.decodeSkipped), count(counters.decodePartial), count(counters.facadeMismatches), count(counters.verifyDecodes), count(counters.verifyDecodeMismatches)); append("[draw-cache] stage data hits (10 s): %llu stages (%llu words refreshed), verified %llu; stage inserts with data positions %llu (%.1f positions each)\n", count(counters.stageDataHits), count(counters.stageDataWords), count(counters.stageDataVerified), count(counters.stageDataInserts), counters.stageDataInserts != 0 ? static_cast(counters.stageDataPositions) / static_cast(counters.stageDataInserts) : 0.0); return text; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index f0e0b777f2..80439c337e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -179,6 +179,10 @@ ShaderMemory::~ShaderMemory() { } } +std::size_t ShaderMemory::Bytes() const { + return sizeof(ShaderMemory) + initial.capacity() * sizeof(initial[0]) + pages.capacity() * sizeof(pages[0]) + pages.size() * sizeof(Page) + supplied.capacity() * sizeof(Supplied) + spilled.capacity() * sizeof(spilled[0]); +} + void ShaderMemory::WordMask::SetRun(std::size_t first, std::size_t count) { while (count != 0) { const auto shift = first % 64u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp index 765947dc99..104fb882b6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ #include #include #include +#include #include namespace { @@ -66,6 +68,14 @@ constexpr std::array WideSlots{SrtSlots - 1u, SrtSlots - 2u, Srt constexpr std::array WideTableCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x9310b010u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; constexpr std::array TableCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; +constexpr std::size_t VertexOffset = 256; +constexpr std::size_t PixelOffset = 512; +constexpr std::size_t DrawSrtBytes = 64; +constexpr std::size_t DrawKeys = SrtSlots * SrtBytes / DrawSrtBytes; +constexpr std::size_t ColorBytes = 1024; +constexpr std::array TriangleVertexCode{0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; +constexpr std::array TablePixelCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xd7650002u, 0x000100c1u, 0x7e200502u, 0x8f108510u, 0xbf8cc07fu, 0xf42c0502u, 0x20000000u, 0x7e000280u, 0x7e020280u, 0xbf8cc07fu, 0xf09c0f08u, 0x00450000u, 0xbf8c3f70u, 0xf800180fu, 0x03020100u, 0xbf810000u}; + class GuestBlock { public: GuestBlock() { @@ -166,6 +176,95 @@ void SrtMovingPerDispatch(const GuestBlock& block) { Require(most <= Driver::DispatchCacheBytes(), "image table cache bounds: dispatches reading a 2 MiB table through " + std::to_string(SrtSlots) + " SRTs kept " + std::to_string(most >> 20u) + " MiB of captured words in the dispatch cache, over its budget of " + std::to_string(Driver::DispatchCacheBytes() >> 20u) + " MiB"); } +void Draw(const GuestBlock& block, std::size_t key) { + const auto color = block.At(OutputOffset); + const auto vertex = block.At(VertexOffset); + const auto pixel = block.At(PixelOffset); + const auto srt = block.At(SrtOffset + key * DrawSrtBytes); + const std::vector> context{ + {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {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}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {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, 0}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 2u << 1u}, + {0x00c, static_cast(srt)}, {0x00d, static_cast(srt >> 32u)}, + }; + std::vector words; + const auto append = [&](const std::vector& packet) { words.insert(words.end(), packet.begin(), packet.end()); }; + append(Pm4(0x79, {0x242, 4})); + for (const auto& [offset, value] : context) append(Pm4(0x69, {offset, value})); + for (const auto& [offset, value] : shader) append(Pm4(0x76, {offset, value})); + append(Pm4(0x2d, {3, 2})); + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "image table draw cache bounds: the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +unsigned BrightestColor(const GuestBlock& block) { + AgcDriver::GuestMemory::FlushGpuWrites(block.At(OutputOffset), ColorBytes); + const auto* bytes = block.Bytes(OutputOffset); + return *std::max_element(bytes, bytes + ColorBytes); +} + +void SrtMovingPerDraw(const GuestBlock& block) { + using AgcDriver::DriverDetail::Driver; + std::memcpy(block.Bytes(VertexOffset), TriangleVertexCode.data(), sizeof(TriangleVertexCode)); + std::memcpy(block.Bytes(PixelOffset), TablePixelCode.data(), sizeof(TablePixelCode)); + const auto fill = [&](std::uint32_t surface) { + for (std::uint32_t record = 0; record < Records; ++record) SetRecord(block, record, surface * Aliases); + }; + fill(3u); + for (std::size_t key = 0; key < DrawKeys; ++key) { + auto* srt = reinterpret_cast(block.Bytes(SrtOffset + key * DrawSrtBytes)); + std::fill(srt, srt + DrawSrtBytes / 4u, 0u); + const auto heap = V(block.At(HeapOffset + key * Stride), Stride, Records - static_cast(key)); + std::copy(heap.begin(), heap.end(), srt); + } + auto& driver = Driver::Get(); + const auto budget = Driver::DrawCacheBytes(); + const auto before = driver.DrawCacheCounters(); + std::uint64_t most = 0; + std::size_t entries = 0; + for (std::size_t key = 0; key < DrawKeys; ++key) { + for (std::size_t sight = 0; sight < 2; ++sight) { + Draw(block, key); + const auto size = driver.DrawCacheSize(); + Require(size.variantBytes <= budget, "image table draw cache bounds: after draw " + std::to_string(2 * key + sight) + " the draw cache kept " + std::to_string(size.entries) + " entries holding " + std::to_string(size.variantBytes >> 10u) + " KiB, over its budget of " + std::to_string(budget >> 10u) + " KiB"); + most = std::max(most, size.variantBytes); + entries = std::max(entries, size.entries); + } + } + const auto filled = driver.DrawCacheCounters(); + const auto inserts = filled.inserts - before.inserts; + const auto evictions = filled.evictions - before.evictions; + const auto byteEvictions = filled.byteEvictions - before.byteEvictions; + const auto counts = " (" + std::to_string(inserts) + " inserts, " + std::to_string(evictions) + " evictions, " + std::to_string(byteEvictions) + " over the byte budget, at most " + std::to_string(entries) + " entries holding " + std::to_string(most >> 10u) + " KiB)"; + Require(BrightestColor(block) == 3u, "image table draw cache bounds: the draws through the table wrote " + std::to_string(BrightestColor(block)) + ", expected 3"); + Require(entries >= 2 && inserts >= DrawKeys / 2, "image table draw cache bounds: the draws were not kept in the draw cache" + counts); + Require(inserts * (most / entries) > 2 * budget, "image table draw cache bounds: the draws did not insert more than twice the budget" + counts); + Require(byteEvictions >= 1 && byteEvictions == evictions, "image table draw cache bounds: the draw cache was not bounded by its byte budget" + counts); + + fill(9u); + std::memset(block.Bytes(OutputOffset), 0, ColorBytes); + Draw(block, 0); + const auto rebuilt = driver.DrawCacheCounters(); + Require(rebuilt.absent - filled.absent == 1 && rebuilt.inserts - filled.inserts == 1, "image table draw cache bounds: the first key, evicted, was not looked up without an entry and inserted again (" + std::to_string(rebuilt.absent - filled.absent) + " absent, " + std::to_string(rebuilt.inserts - filled.inserts) + " inserts)"); + Require(BrightestColor(block) == 9u, "image table draw cache bounds: the evicted key drawn again wrote " + std::to_string(BrightestColor(block)) + ", expected 9"); + std::memset(block.Bytes(OutputOffset), 0, ColorBytes); + Draw(block, 0); + const auto hit = driver.DrawCacheCounters(); + Require(hit.hits - rebuilt.hits == 1, "image table draw cache bounds: the evicted key, inserted again, was not hit by its next draw"); + Require(BrightestColor(block) == 9u, "image table draw cache bounds: the hit on the rebuilt key wrote " + std::to_string(BrightestColor(block)) + ", expected 9"); + std::puts(("image table draw cache bounds passed" + counts).c_str()); +} + std::array TextureWords(const GuestBlock& block, std::uint32_t element) { const auto address = block.At(TexturesOffset + static_cast(element / Aliases) * TextureBytes); return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 7u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, element % Aliases, 0u}; @@ -337,9 +436,11 @@ void SetBudgets() { #ifdef _WIN32 _putenv_s("APS5_RESOURCE_CACHE_TABLE_ELEMENTS", "8192"); _putenv_s("APS5_DISPATCH_CACHE_BYTES", "16777216"); + _putenv_s("APS5_DRAW_CACHE_BYTES", "16777216"); #else setenv("APS5_RESOURCE_CACHE_TABLE_ELEMENTS", "8192", 1); setenv("APS5_DISPATCH_CACHE_BYTES", "16777216", 1); + setenv("APS5_DRAW_CACHE_BYTES", "16777216", 1); #endif } @@ -398,6 +499,7 @@ int main(int argc, char** argv) { } TablesChangingPerDispatch(block); SrtMovingPerDispatch(block); + SrtMovingPerDraw(block); AgcDriverShutdown_nid_postfix(); std::puts("image table cache bounds tests passed"); return 0; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 27fc474b9c..640010cdbc 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -116,6 +116,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DISPMOVE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1024 | upper bound in KiB on the read-only buffers a dispatch-cache hit moves into its template, above which the dispatch is prepared again | | `APS5_DRAIN_ALL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | drains every packet, disabling the batched recording path | | `APS5_DRAIN_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records completion labels immediately instead of deferring them with the batch, and drains a label recorded while a copied write-back is pending instead of storing it behind the write-backs | +| `APS5_DRAW_CACHE_BYTES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | 268435456 | size of the draw cache in bytes, evicting least-recently-used entries past it (0 keeps the default of 256 MiB) | | `APS5_DRAW_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | 4096 | number of entries in the draw cache; 0 keeps the default 4096 | | `APS5_DRAW_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | treats draw packets as drains, waiting for them instead of recording ahead | | `APS5_DRAW_INPUTS_IN_PLACE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | binds draw vertex and index ranges in place from host imports also with NVIDIA's Windows driver, which copies them by default | From 79fef78d01c7b5da0ae71e4f9bb45e8709915734 Mon Sep 17 00:00:00 2001 From: Dragos Negoita Date: Sat, 24 Oct 2026 11:48:45 +0300 Subject: [PATCH 024/702] fix(relinker): honor prefixes in the syscall number byte walk The raw-byte walk behind the refusal message ignored the prefixes of the constant rax loads it recognizes. mov r8d, imm32 (41 B8) and xor r8d, eax (41 31 C0) reported the immediate or zero as the syscall number, xor ax, ax (66 31 C0) reported a zeroed rax, and mov r8b, imm8 (41 B0) misaligned the walk so an established number was not reported. An active REX prefix is only the byte immediately before the opcode, and a REX byte earlier in the prefix run is dead, so 41 66 B8 is a mov ax, imm16 with one dead byte in front. The B8 arm accepts a REX without W or B, since rex.R and rex.X do not change the register, and rejects an operand-size override; the xor arm rejects a 16-bit pair and the REX bit that extends the destination, B for the 31 form and R for the 33 form; the B0 arm treats REX.B as a different register and walks past the three bytes; the 66 B8 arm keeps the dead REX byte in the walked span. Test the reported and skipped byte patterns on every target. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .../relinker/src/analysis/SyscallScanner.cpp | 56 ++++++++++++++++--- .../relinker/tests/test_sce_code_gaps.py | 15 +++++ 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 6003cda8e4..ca50a29a16 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -177,6 +177,32 @@ static std::optional ConstantSyscallNumber(const std::vector& code, const std::size_t opcode) { + ActivePrefixes prefixes{false, 0, 0}; + std::size_t start = opcode; + if (start > 0 && (code[start - 1] & 0xF0) == 0x40) { + prefixes.Rex = code[start - 1]; + --start; + } + while (start > 0) { + const std::uint8_t prefix = code[start - 1]; + if (prefix == 0x66) + prefixes.OperandSize = true; + else if (prefix != 0x67 && prefix != 0xF0 && prefix != 0xF2 && prefix != 0xF3 && + prefix != 0x26 && prefix != 0x2E && prefix != 0x36 && prefix != 0x3E && prefix != 0x64 && prefix != 0x65) + break; + --start; + } + prefixes.Count = opcode - start; + return prefixes; +} + static std::optional ConstantRaxSetEndingAt(const std::vector& code, const std::size_t end) { if (end >= 10 && code[end - 10] == 0x48 && code[end - 9] == 0xB8) { std::uint64_t value = 0; @@ -189,11 +215,19 @@ static std::optional ConstantRaxSetEndingAt(const std::vector(static_cast(static_cast(immediate))); } if (end >= 5 && code[end - 5] == 0xB8) { - std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); - return immediate; + const ActivePrefixes prefixes = ActivePrefixesBefore(code, end - 5); + if (!prefixes.OperandSize && (prefixes.Rex & 0x09) == 0) { + std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); + return immediate; + } + } + if (end >= 2 && (code[end - 2] == 0x31 || code[end - 2] == 0x33) && code[end - 1] == 0xC0) { + const ActivePrefixes prefixes = ActivePrefixesBefore(code, end - 2); + const bool wordSized = prefixes.OperandSize && (prefixes.Rex & 0x08) == 0; + const bool extendedDestination = code[end - 2] == 0x31 ? (prefixes.Rex & 0x01) != 0 : (prefixes.Rex & 0x04) != 0; + if (!wordSized && !extendedDestination) + return 0; } - if (end >= 2 && (code[end - 2] == 0x31 || code[end - 2] == 0x33) && code[end - 1] == 0xC0) - return 0; return std::nullopt; } @@ -204,10 +238,16 @@ struct LowRaxWrite { }; static std::optional ImmediateLowRaxWriteEndingAt(const std::vector& code, const std::size_t end) { - if (end >= 2 && code[end - 2] == 0xB0) - return LowRaxWrite{2, 0xFF, code[end - 1]}; - if (end >= 4 && code[end - 4] == 0x66 && code[end - 3] == 0xB8) - return LowRaxWrite{4, 0xFFFF, static_cast(code[end - 2]) | static_cast(code[end - 1]) << 8}; + if (end >= 2 && code[end - 2] == 0xB0) { + const ActivePrefixes prefixes = ActivePrefixesBefore(code, end - 2); + if ((prefixes.Rex & 0x01) == 0) + return LowRaxWrite{prefixes.Count + 2, 0xFF, code[end - 1]}; + return LowRaxWrite{prefixes.Count + 2, 0, 0}; + } + if (end >= 4 && code[end - 4] == 0x66 && code[end - 3] == 0xB8) { + const std::size_t length = end >= 5 && (code[end - 5] & 0xF0) == 0x40 ? 5 : 4; + return LowRaxWrite{length, 0xFFFF, static_cast(code[end - 2]) | static_cast(code[end - 1]) << 8}; + } return std::nullopt; } diff --git a/core/relinker/relinker/tests/test_sce_code_gaps.py b/core/relinker/relinker/tests/test_sce_code_gaps.py index aee8daf982..c3971a0b1a 100644 --- a/core/relinker/relinker/tests/test_sce_code_gaps.py +++ b/core/relinker/relinker/tests/test_sce_code_gaps.py @@ -29,6 +29,13 @@ LOADED_NUMBERS = ( ("low-byte-after-zeroing", bytes.fromhex("31 c0 b0 3b 0f 05 c3"), 0x214, 59), ("low-word-after-zeroing", bytes.fromhex("31 c0 66 b8 3b 01 0f 05 c3"), 0x216, 0x13B), ("eight-register-moves", bytes.fromhex("b8 3b 00 00 00") + bytes.fromhex("89 db") * 8 + SYSCALL, 0x225, 59), + ("xor-eax-r8d", bytes.fromhex("41 33 c0 0f 05 c3"), 0x213, 0), + ("xor-eax-extended-source", bytes.fromhex("44 31 c0 0f 05 c3"), 0x213, 0), + ("rex-w-xor", bytes.fromhex("48 31 c0 0f 05 c3"), 0x213, 0), + ("mov-eax-rex-r", bytes.fromhex("44 b8 3b 00 00 00 0f 05 c3"), 0x216, 59), + ("load-then-mov-r8b", bytes.fromhex("b8 3b 00 00 00 41 b0 05 0f 05 c3"), 0x218, 59), + ("xor-then-mov-r8b", bytes.fromhex("31 c0 41 b0 05 0f 05 c3"), 0x215, 0), + ("word-load-dead-rex", bytes.fromhex("31 c0 41 66 b8 3b 01 0f 05 c3"), 0x217, 0x13B), ) JUMP_TABLE_DISPATCH = bytes.fromhex("48 8d 0d 09 00 00 00 48 63 04 b9 48 01 c8 ff e0") @@ -132,6 +139,14 @@ def main(): expected_output=SKIPPED % (1, 0x218)) run(relinker, work, "sce-gap-syscall-low-byte-only", executable(bytes.fromhex("b0 3b 0f 05 c3")), [], expected_output=SKIPPED % (1, 0x212)) + run(relinker, work, "sce-gap-syscall-number-mov-r8d", executable(bytes.fromhex("41 b8 05 00 00 00 0f 05 c3")), [], + expected_output=SKIPPED % (1, 0x216)) + run(relinker, work, "sce-gap-syscall-number-xor-r8d-eax", executable(bytes.fromhex("41 31 c0 0f 05 c3")), [], + expected_output=SKIPPED % (1, 0x213)) + run(relinker, work, "sce-gap-syscall-number-word-zeroed", executable(bytes.fromhex("66 31 c0 0f 05 c3")), [], + expected_output=SKIPPED % (1, 0x213)) + run(relinker, work, "sce-gap-syscall-number-word-zeroed-register-form", executable(bytes.fromhex("66 33 c0 0f 05 c3")), [], + expected_output=SKIPPED % (1, 0x213)) run(relinker, work, "sce-gap-syscall-after-overlong-instruction", executable(SYSCALL_NUMBER + b"\x41" * 15 + SYSCALL), [], expected_output=SKIPPED % (1, 0x217)) run_module(relinker, work, "ps5-module-gap-syscall-bytes", ps5_module(), [], From 4201b8212a0c4d6a0b09b6274467192993c80ebb Mon Sep 17 00:00:00 2001 From: Mouhand Kaddo <73709175+Mouhand-Kaddo@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:42:25 +0400 Subject: [PATCH 025/702] fix(agc): read committed guest pages on Windows without faulting on released pages ReadCommitted and EqualsCommitted find the readable pages of a range, then copy or compare them. On Windows a page another thread releases in between faulted the copy. Demon's Souls (PPSA01342) samples a texture at 0x38400 of 352 MiB while loading the tutorial level; on Windows that range covers host heap pages, and the load stopped with an access violation in ucrtbase's memcpy under cachedTexture. On Windows the pages are now read through ReadProcessMemory, page by page after a failed whole read, and a page that cannot be read counts as uncommitted: zero in a copy, left out of a comparison. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/GuestMemory.cpp | 56 +++++++++++++++++-- .../prx/libSceAgcDriver/tests/BdaTests.hpp | 1 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 1 + .../tests/GuestAllocations.cpp | 32 +++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 5 files changed, 87 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 8648a7fa14..0d46f39cf9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -2548,13 +2548,61 @@ void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) { namespace { +#ifdef _WIN32 +bool readPages(std::byte* destination, std::uint64_t source, std::size_t bytes) { + SIZE_T copied = 0; + return ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(source), destination, bytes, &copied) != 0 && copied == bytes; +} + +template +void visitReadablePages(std::byte* destination, std::uint64_t source, std::size_t bytes, Visit&& visit) { + if (readPages(destination, source, bytes)) { + visit(std::size_t{0}, bytes, true); + return; + } + for (std::size_t offset = 0; offset < bytes;) { + const auto chunk = std::min(bytes - offset, PageBytes - static_cast((source + offset) % PageBytes)); + visit(offset, chunk, readPages(destination + offset, source + offset, chunk)); + offset += chunk; + } +} +#endif + +void copyCommittedBytes(std::byte* destination, std::uint64_t source, std::size_t bytes) { +#ifdef _WIN32 + visitReadablePages(destination, source, bytes, [&](std::size_t offset, std::size_t count, bool read) { + if (!read) std::memset(destination + offset, 0, count); + }); +#else + std::memcpy(destination, reinterpret_cast(source), bytes); +#endif +} + +bool equalsCommittedBytes(std::uint64_t address, const std::byte* expected, std::size_t bytes) { +#ifdef _WIN32 + constexpr std::size_t ChunkBytes = 64 * PageBytes; + thread_local std::vector current(ChunkBytes); + for (std::size_t done = 0; done < bytes; done += ChunkBytes) { + const auto count = std::min(ChunkBytes, bytes - done); + bool equal = true; + visitReadablePages(current.data(), address + done, count, [&](std::size_t offset, std::size_t length, bool read) { + if (read && equal && std::memcmp(current.data() + offset, expected + done + offset, length) != 0) equal = false; + }); + if (!equal) return false; + } + return true; +#else + return std::memcmp(reinterpret_cast(address), expected, bytes) == 0; +#endif +} + void copyCommitted(std::uint64_t address, std::span destination) { if (Accessible(reinterpret_cast(address), destination.size())) { - std::memcpy(destination.data(), reinterpret_cast(address), destination.size()); + copyCommittedBytes(destination.data(), address, destination.size()); return; } std::memset(destination.data(), 0, destination.size()); - for (const auto& [begin, end] : CommittedRanges(address, destination.size())) std::memcpy(destination.data() + (begin - address), reinterpret_cast(begin), static_cast(end - begin)); + for (const auto& [begin, end] : CommittedRanges(address, destination.size())) copyCommittedBytes(destination.data() + (begin - address), begin, static_cast(end - begin)); } } @@ -2585,9 +2633,9 @@ bool EqualsCommitted(std::uint64_t address, std::span bytes) { bool EqualsCommittedUnsynced(std::uint64_t address, std::span bytes) { if (bytes.empty()) return true; const TimedAccess timed(CounterCompare, bytes.size()); - if (Accessible(reinterpret_cast(address), bytes.size())) return std::memcmp(reinterpret_cast(address), bytes.data(), bytes.size()) == 0; + if (Accessible(reinterpret_cast(address), bytes.size())) return equalsCommittedBytes(address, bytes.data(), bytes.size()); for (const auto& [begin, end] : CommittedRanges(address, bytes.size())) { - if (std::memcmp(reinterpret_cast(begin), bytes.data() + (begin - address), static_cast(end - begin)) != 0) return false; + if (!equalsCommittedBytes(begin, bytes.data() + (begin - address), static_cast(end - begin))) return false; } return true; } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 49cd994d6e..9a14c50465 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -24,6 +24,7 @@ void RunHeapMirrorRunUnwatchTests(const AgcDriver::Graphics::Context& context, c void RunGuestAllocationTests(); void RunGuestLeaseWaitTests(); void RunUnmappedGapTests(); +void RunVanishedPageReadTests(); void RunColorTargetLayoutTests(); void RunLiveStackAccessTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index ffa2c44a13..a8177c49a2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -12107,6 +12107,7 @@ int main(int argc, char** argv) { Require(mock.live == 0, "BDA resources leaked Vulkan objects"); RunGuestAllocationTests(); RunUnmappedGapTests(); + RunVanishedPageReadTests(); RunColorTargetLayoutTests(); RunLiveStackAccessTests(); RunTextureFormatTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index 89d83afd29..8d8a41e94a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -328,3 +329,34 @@ void RunUnmappedGapTests() { GuestArena::GuestArenaRelease_nid_postfix(spare, page); #endif } + +void RunVanishedPageReadTests() { +#ifdef _WIN32 + namespace GuestMemory = AgcDriver::GuestMemory; + if (!GuestArena::GuestArenaAvailable_nid_postfix()) return; + constexpr std::size_t page = 4096; + constexpr std::uint32_t ReadWriteProtection = 0x04; + auto* block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * page, page)); + const auto address = reinterpret_cast(block); + GuestArena::GuestArenaCommit_nid_postfix(block, page, ReadWriteProtection, page); + GuestArena::GuestArenaCommit_nid_postfix(block + page, page, ReadWriteProtection, page); + std::memset(block, 0x5a, 2 * page); + Require(GuestMemory::Accessible(block, 2 * page), "committed arena test pages are not readable"); + GuestArena::GuestArenaReset_nid_postfix(block + page, page); + std::array read{}; + read.fill(std::byte{0xff}); + GuestMemory::ReadCommitted(address, read); + const auto filled = [](std::span bytes, std::byte value) { return std::all_of(bytes.begin(), bytes.end(), [&](std::byte byte) { return byte == value; }); }; + Require(filled(std::span(read).first(page), std::byte{0x5a}) && filled(std::span(read).subspan(page), std::byte{0}), "a committed read of a page released after it was checked did not read it as uncommitted"); + std::array expected{}; + std::fill(expected.begin(), expected.begin() + page, std::byte{0x5a}); + std::fill(expected.begin() + page, expected.end(), std::byte{0x77}); + Require(GuestMemory::EqualsCommitted(address, expected), "a committed compare of a page released after it was checked did not skip it"); + expected[page - 1] = std::byte{0}; + Require(!GuestMemory::EqualsCommitted(address, expected), "a committed compare missed a difference before a page released after it was checked"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(address, 2 * page); + GuestArena::GuestArenaReset_nid_postfix(block, page); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(address, 2 * page); + GuestArena::GuestArenaRelease_nid_postfix(block, 2 * page); +#endif +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 63766b2d7e..c8ff3c1715 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -232,6 +232,7 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. - [Device-local staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written or atomic buffer element inside a host import, including one an address-based compute build reaches through its pointers, is copied to device memory before the dispatch and copied back whole after it, so a CPU store into that range in between is rolled back. +- [Committed reads of released pages](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) - `ReadCommitted` and `EqualsCommitted` find the readable pages of a range first and read them afterwards. On Windows they read through `ReadProcessMemory`, so a page released in between reads as zero and is left out of the comparison instead of faulting: Demon's Souls (PPSA01342) samples a texture at 0x38400 of 352 MiB while it loads the tutorial level, a range that holds no guest allocation and on Windows covers host heap pages other threads release, which stopped it with an access violation. A texture outside every registered guest allocation still takes the host bytes found there, as it did before; what the console returns for it is not measured. On Linux the pages are copied directly, so a page released between the check and the copy still faults. - [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. - [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. From 74285a827eea3a9371e924b0029c1661d5da60a5 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 13:09:02 +0200 Subject: [PATCH 026/702] fix(tools): reject unsupported inner ELF versions --- tools/tests/test_unwrap_self.py | 57 +++++++++++++++++++++++++++++++++ tools/unwrap_self.py | 3 ++ 2 files changed, 60 insertions(+) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 035a38e71f..38edec0c90 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -663,6 +663,63 @@ class UnwrapTest(unittest.TestCase): self.assertNotIn("Traceback", result.stderr) self.assertFalse(output.exists()) + def test_unsupported_inner_elf_versions_are_refused(self): + for identification in (0, 1, 2, 255): + for version in (0, 1, 2, 0x1000001, 0xFFFFFFFF): + if identification == version == 1: + continue + with self.subTest(identification=identification, version=version): + container = bytearray(build_container(default_segments(), default_records(), default_payloads())) + container[ELF_HEADER_OFFSET + 6] = identification + struct.pack_into(" Date: Sat, 24 Oct 2026 13:32:46 +0200 Subject: [PATCH 027/702] fix(tools): report excessive summary JSON nesting --- tools/performance_compare.py | 4 +- tools/tests/test_performance_compare.py | 53 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/tools/performance_compare.py b/tools/performance_compare.py index 6a6c599351..429a0858ba 100644 --- a/tools/performance_compare.py +++ b/tools/performance_compare.py @@ -73,7 +73,7 @@ def load_summary(path, trace=None): record = json.loads(line) if not isinstance(record, dict): raise ValueError("summary must be an object") - except ValueError as error: + except (ValueError, RecursionError) as error: raise ValueError(f"{path}:{line_number}: {error}") from error if trace is None or record.get("trace") == trace: records.append((line_number, record)) @@ -182,7 +182,7 @@ def main(argv=None): if args.require_matching_context and report["configuration_differences"]: raise ValueError("recorded configuration differs: " + ", ".join(report["configuration_differences"])) output = json.dumps(report, indent=2, ensure_ascii=False, allow_nan=False) + "\n" if args.json else markdown(report) - except (OSError, ValueError, OverflowError) as error: + except (OSError, ValueError, OverflowError, RecursionError) as error: parser.exit(2, f"{parser.prog}: {error}\n") sys.stdout.write(output) diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py index 4016f14439..a5079fe48d 100644 --- a/tools/tests/test_performance_compare.py +++ b/tools/tests/test_performance_compare.py @@ -1,4 +1,5 @@ import copy +import io import json import os import subprocess @@ -177,6 +178,58 @@ class PerformanceCompareTests(unittest.TestCase): self.assertEqual(result.stdout, "") self.assertIn("finite numeric range", result.stderr) + def test_excessive_json_nesting_reports_the_input_location(self): + depth = 100000 + for opening, closing in (("[", "]"), ('{"nested":', "}")): + with self.subTest(opening=opening): + record = json.dumps(self.baseline)[:-1] + ', "extra": ' + opening * depth + "0" + closing * depth + "}" + self.before.write_text("\n\n" + record + "\n", encoding="utf-8") + with self.assertRaisesRegex(ValueError, r"before.jsonl:3:") as error: + performance_compare.load_summary(self.before) + self.assertIsInstance(error.exception.__cause__, RecursionError) + + def test_excessive_json_nesting_produces_only_a_cli_error(self): + depth = 100000 + for path in (self.before, self.after): + for opening, closing in (("[", "]"), ('{"nested":', "}")): + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + record = json.dumps(self.baseline)[:-1] + ', "extra": ' + opening * depth + "0" + closing * depth + "}" + path.write_text("\n\n" + record + "\n", encoding="utf-8") + original = (self.before.read_bytes(), self.after.read_bytes()) + for args in ((), ("--json",)): + with self.subTest(path=path.name, opening=opening, args=args): + result = self.run_cli(*args) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + self.assertIn(f"{path}:3:", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual((self.before.read_bytes(), self.after.read_bytes()), original) + + def test_json_report_recursion_produces_only_a_cli_error(self): + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + with patch.object(performance_compare.json, "dumps", side_effect=RecursionError("report nesting is too deep")), \ + patch.object(sys, "stdout", new_callable=io.StringIO) as output, \ + patch.object(sys, "stderr", new_callable=io.StringIO) as errors: + with self.assertRaises(SystemExit) as error: + performance_compare.main([str(self.before), str(self.after), "--json"]) + self.assertEqual(error.exception.code, 2) + self.assertEqual(output.getvalue(), "") + self.assertIn("report nesting is too deep", errors.getvalue()) + + def test_supported_nested_metadata_is_preserved(self): + metadata = {"value": 7} + for depth in range(16): + metadata = {"nested": [metadata]} + self.baseline["extra"] = metadata + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli("--json") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout)["baseline"]["extra"], metadata) + self.assertEqual(self.run_cli().returncode, 0) + def test_markdown_reports_deltas_and_escapes_table_values(self): self.candidate["frame_ms"]["max"] = 60.0 self.candidate["gpu"] = "GPU | \nline" From 1f6c936b36446eeadc0c1e0f8bd37a4de8c65aa0 Mon Sep 17 00:00:00 2001 From: Dragos Negoita Date: Sat, 24 Oct 2026 15:37:39 +0300 Subject: [PATCH 028/702] fix(relinker): reject XORs with extended operands Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .../relinker/src/analysis/SyscallScanner.cpp | 4 ++-- core/relinker/relinker/tests/test_sce_code_gaps.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index ca50a29a16..7893bd4017 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -224,8 +224,8 @@ static std::optional ConstantRaxSetEndingAt(const std::vector= 2 && (code[end - 2] == 0x31 || code[end - 2] == 0x33) && code[end - 1] == 0xC0) { const ActivePrefixes prefixes = ActivePrefixesBefore(code, end - 2); const bool wordSized = prefixes.OperandSize && (prefixes.Rex & 0x08) == 0; - const bool extendedDestination = code[end - 2] == 0x31 ? (prefixes.Rex & 0x01) != 0 : (prefixes.Rex & 0x04) != 0; - if (!wordSized && !extendedDestination) + const bool extendedOperand = (prefixes.Rex & 0x05) != 0; + if (!wordSized && !extendedOperand) return 0; } return std::nullopt; diff --git a/core/relinker/relinker/tests/test_sce_code_gaps.py b/core/relinker/relinker/tests/test_sce_code_gaps.py index c3971a0b1a..9f5452d568 100644 --- a/core/relinker/relinker/tests/test_sce_code_gaps.py +++ b/core/relinker/relinker/tests/test_sce_code_gaps.py @@ -29,11 +29,11 @@ LOADED_NUMBERS = ( ("low-byte-after-zeroing", bytes.fromhex("31 c0 b0 3b 0f 05 c3"), 0x214, 59), ("low-word-after-zeroing", bytes.fromhex("31 c0 66 b8 3b 01 0f 05 c3"), 0x216, 0x13B), ("eight-register-moves", bytes.fromhex("b8 3b 00 00 00") + bytes.fromhex("89 db") * 8 + SYSCALL, 0x225, 59), - ("xor-eax-r8d", bytes.fromhex("41 33 c0 0f 05 c3"), 0x213, 0), - ("xor-eax-extended-source", bytes.fromhex("44 31 c0 0f 05 c3"), 0x213, 0), ("rex-w-xor", bytes.fromhex("48 31 c0 0f 05 c3"), 0x213, 0), + ("rex-x-xor", bytes.fromhex("42 31 c0 0f 05 c3"), 0x213, 0), ("mov-eax-rex-r", bytes.fromhex("44 b8 3b 00 00 00 0f 05 c3"), 0x216, 59), ("load-then-mov-r8b", bytes.fromhex("b8 3b 00 00 00 41 b0 05 0f 05 c3"), 0x218, 59), + ("load-then-xor-r8d-r8d", bytes.fromhex("b8 3b 00 00 00 47 31 c0 0f 05 c3"), 0x218, 59), ("xor-then-mov-r8b", bytes.fromhex("31 c0 41 b0 05 0f 05 c3"), 0x215, 0), ("word-load-dead-rex", bytes.fromhex("31 c0 41 66 b8 3b 01 0f 05 c3"), 0x217, 0x13B), ) @@ -143,6 +143,14 @@ def main(): expected_output=SKIPPED % (1, 0x216)) run(relinker, work, "sce-gap-syscall-number-xor-r8d-eax", executable(bytes.fromhex("41 31 c0 0f 05 c3")), [], expected_output=SKIPPED % (1, 0x213)) + run(relinker, work, "sce-gap-syscall-number-xor-r8d-eax-register-form", executable(bytes.fromhex("44 33 c0 0f 05 c3")), [], + expected_output=SKIPPED % (1, 0x213)) + run(relinker, work, "sce-gap-syscall-number-xor-eax-r8d", executable(bytes.fromhex("41 33 c0 0f 05 c3")), [], + expected_output=SKIPPED % (1, 0x213)) + run(relinker, work, "sce-gap-syscall-number-xor-eax-r8d-swapped", executable(bytes.fromhex("44 31 c0 0f 05 c3")), [], + expected_output=SKIPPED % (1, 0x213)) + run(relinker, work, "sce-gap-syscall-number-load-then-xor-eax-r8d", executable(bytes.fromhex("b8 3b 00 00 00 41 33 c0 0f 05 c3")), [], + expected_output=SKIPPED % (1, 0x218)) run(relinker, work, "sce-gap-syscall-number-word-zeroed", executable(bytes.fromhex("66 31 c0 0f 05 c3")), [], expected_output=SKIPPED % (1, 0x213)) run(relinker, work, "sce-gap-syscall-number-word-zeroed-register-form", executable(bytes.fromhex("66 33 c0 0f 05 c3")), [], From b1db8bb5fdbe901091f403ac9cb14e71612d46d2 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 14:45:54 +0200 Subject: [PATCH 029/702] test(tools): bound summary nesting regression workload --- tools/tests/test_performance_compare.py | 53 ++++++++++++++++--------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py index a5079fe48d..67781ba46b 100644 --- a/tools/tests/test_performance_compare.py +++ b/tools/tests/test_performance_compare.py @@ -1,4 +1,5 @@ import copy +from functools import lru_cache import io import json import os @@ -28,6 +29,19 @@ IDENTITY = {"cpu": 'CPU "X"', "cpu_threads": 16, "memory_mib": 32000, "title_id": "PPSA00000", "title": "Example", "content_version": "01.002.000"} +@lru_cache(maxsize=2) +def excessive_json_value(opening, closing): + depth = sys.getrecursionlimit() + 64 + for attempt in range(8): + value = opening * depth + "0" + closing * depth + try: + json.loads(value) + except RecursionError: + return opening * (depth + 128) + "0" + closing * (depth + 128) + depth *= 2 + raise AssertionError("JSON decoder accepted every bounded nesting probe") + + class PerformanceCompareTests(unittest.TestCase): def setUp(self): self.directory = tempfile.TemporaryDirectory() @@ -179,32 +193,30 @@ class PerformanceCompareTests(unittest.TestCase): self.assertIn("finite numeric range", result.stderr) def test_excessive_json_nesting_reports_the_input_location(self): - depth = 100000 for opening, closing in (("[", "]"), ('{"nested":', "}")): with self.subTest(opening=opening): - record = json.dumps(self.baseline)[:-1] + ', "extra": ' + opening * depth + "0" + closing * depth + "}" + record = json.dumps(self.baseline)[:-1] + ', "extra": ' + excessive_json_value(opening, closing) + "}" self.before.write_text("\n\n" + record + "\n", encoding="utf-8") with self.assertRaisesRegex(ValueError, r"before.jsonl:3:") as error: performance_compare.load_summary(self.before) self.assertIsInstance(error.exception.__cause__, RecursionError) def test_excessive_json_nesting_produces_only_a_cli_error(self): - depth = 100000 - for path in (self.before, self.after): - for opening, closing in (("[", "]"), ('{"nested":', "}")): + cases = ((self.before, "[", "]", ()), (self.before, '{"nested":', "}", ("--json",)), + (self.after, "[", "]", ("--json",)), (self.after, '{"nested":', "}", ())) + for path, opening, closing, args in cases: + with self.subTest(path=path.name, opening=opening, args=args): self.write(self.before, self.baseline) self.write(self.after, self.candidate) - record = json.dumps(self.baseline)[:-1] + ', "extra": ' + opening * depth + "0" + closing * depth + "}" + record = json.dumps(self.baseline)[:-1] + ', "extra": ' + excessive_json_value(opening, closing) + "}" path.write_text("\n\n" + record + "\n", encoding="utf-8") original = (self.before.read_bytes(), self.after.read_bytes()) - for args in ((), ("--json",)): - with self.subTest(path=path.name, opening=opening, args=args): - result = self.run_cli(*args) - self.assertEqual(result.returncode, 2, result.stderr) - self.assertEqual(result.stdout, "") - self.assertIn(f"{path}:3:", result.stderr) - self.assertNotIn("Traceback", result.stderr) - self.assertEqual((self.before.read_bytes(), self.after.read_bytes()), original) + result = self.run_cli(*args) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + self.assertIn(f"{path}:3:", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual((self.before.read_bytes(), self.after.read_bytes()), original) def test_json_report_recursion_produces_only_a_cli_error(self): self.write(self.before, self.baseline) @@ -225,10 +237,15 @@ class PerformanceCompareTests(unittest.TestCase): self.baseline["extra"] = metadata self.write(self.before, self.baseline) self.write(self.after, self.candidate) - result = self.run_cli("--json") - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(json.loads(result.stdout)["baseline"]["extra"], metadata) - self.assertEqual(self.run_cli().returncode, 0) + for args in ((), ("--json",)): + with self.subTest(args=args), patch.object(sys, "stdout", new_callable=io.StringIO) as output, \ + patch.object(sys, "stderr", new_callable=io.StringIO) as errors: + performance_compare.main([str(self.before), str(self.after), *args]) + self.assertEqual(errors.getvalue(), "") + if args: + self.assertEqual(json.loads(output.getvalue())["baseline"]["extra"], metadata) + else: + self.assertIn("# Performance comparison", output.getvalue()) def test_markdown_reports_deltas_and_escapes_table_values(self): self.candidate["frame_ms"]["max"] = 60.0 From e21db2912e2dbb11aeb3666eab5b095403d2c917 Mon Sep 17 00:00:00 2001 From: Maximilian Fleckenstein <166870690+maximilianfv@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:54:35 -0300 Subject: [PATCH 030/702] fix(libSceNpTrophy2): include for std::size_t in the constants header NpTrophy2Constants.hpp uses std::size_t but includes only . libstdc++ happens to declare it through that header; libc++ does not, so guest_nptrophy2_tests fails to compile on macOS with "no type named 'size_t' in namespace 'std'". Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp index 58ec7f5e33..547751437a 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_NPTROPHY2CONSTANTS_HPP #define CORE_LIBS_PRX_LIBSCENPTROPHY2_NPTROPHY2CONSTANTS_HPP +#include #include static constexpr int SCE_NP_TROPHY2_OK = 0; From 76008e99b9bf02bcbcbec2ed0d3c5380204cde2b Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:47:31 +0200 Subject: [PATCH 031/702] test(tools): compare quoted and resolved paths in the patch advisor test The patch advisor prints its conversion options through command_line, which quotes a path containing a space, and passes resolved paths to the relinker, so the unit test failed when the temporary directory had a space in its path or was reached through an 8.3 short name or a symlink. Build the expected text with the same helper, resolve the fixture root and give it a space so every machine exercises the quoted rendering. Co-Authored-By: Claude Fable 5.1 --- tools/tests/test_suggest_patches.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/tests/test_suggest_patches.py b/tools/tests/test_suggest_patches.py index 0da25c6f5d..18f1028ee8 100644 --- a/tools/tests/test_suggest_patches.py +++ b/tools/tests/test_suggest_patches.py @@ -17,9 +17,9 @@ VERIFIED = "Verified 1 patches: 1 ok, 0 not-checked, 0 mismatch\n" class PatchAdvisorTests(unittest.TestCase): def setUp(self): - self.directory = tempfile.TemporaryDirectory() + self.directory = tempfile.TemporaryDirectory(prefix="patch advisor-") self.addCleanup(self.directory.cleanup) - self.root = Path(self.directory.name) + self.root = Path(self.directory.name).resolve() self.catalog = self.root / "catalog" self.catalog.mkdir() self.input = self.root / "eboot.bin" @@ -229,7 +229,7 @@ class PatchAdvisorTests(unittest.TestCase): self.assertIn("--patches", output.getvalue()) self.assertIn("--module-dir Media/Modules", output.getvalue()) self.assertIn("--executable picker/picker.bin", output.getvalue()) - self.assertIn(f"--sce-module-path {self.root}", output.getvalue()) + self.assertIn(suggest_patches.command_line(["--sce-module-path", str(self.root)]), output.getvalue()) @patch.object(suggest_patches.subprocess, "run") def test_no_entries_for_title_is_distinct(self, run): From d63c9533a7cc6370ad6c4d9c87688429a650676b Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:47:29 +0200 Subject: [PATCH 032/702] fix(relinker): name R_X86_64_PC32 in the import registry relocationTypeName had no case for R_X86_64_PC32, which ValidationPolicy accepts, so a PC32 import appeared in the Linux registry as UNKNOWN(2). The registry now prints its name like the others. --- .../relinker/src/pipeline/RelinkerPipeline.cpp | 1 + .../relinker/tests/test_call_registry.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 77044446d2..da15124fa2 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -42,6 +42,7 @@ RelinkerPipeline::RelinkerPipeline(std::shared_ptr elfReader, std::s std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { switch (type) { case 1: return "R_X86_64_64"; + case 2: return "R_X86_64_PC32"; case 6: return "R_X86_64_GLOB_DAT"; case 7: return "R_X86_64_JUMP_SLOT"; case 10: return "R_X86_64_32"; diff --git a/core/relinker/relinker/tests/test_call_registry.py b/core/relinker/relinker/tests/test_call_registry.py index 5b9a61ac30..f1042be198 100644 --- a/core/relinker/relinker/tests/test_call_registry.py +++ b/core/relinker/relinker/tests/test_call_registry.py @@ -99,6 +99,22 @@ def main(): needed_json = output_needed_bad.with_suffix(".needed.json") if result_needed_bad.returncode != 2 or b"UTF-8" not in result_needed_bad.stderr or needed_json.exists(): raise AssertionError((result_needed_bad.returncode, result_needed_bad.stdout, result_needed_bad.stderr)) + for relocation_type, type_name in ((1, "R_X86_64_64"), (2, "R_X86_64_PC32"), + (6, "R_X86_64_GLOB_DAT"), (10, "R_X86_64_32"), + (16, "R_X86_64_DTPMOD64"), (17, "R_X86_64_DTPOFF64"), + (18, "R_X86_64_TPOFF64")): + image = make_image(b"\0lib.so\0symbol\0") + struct.pack_into(" Date: Sat, 24 Oct 2026 22:46:57 +0530 Subject: [PATCH 033/702] fix(relinker): collect --to-intel branch targets without signed overflow Branch target collection added the branch displacement to the next instruction address as std::int64_t, which is undefined behaviour for a segment mapped at or above 2^63 (found by fuzzing with UBSan, see #10653). The addresses now add as unsigned values and only targets that wrap past 2^64 are dropped, so the branch-target check also applies to segments mapped across 2^63. Fixes #10653 --- core/relinker/codegen/src/Amd64OnlyConverter.cpp | 12 +++++++----- .../codegen/tests/Amd64OnlyConverterTests.cpp | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index de510c4224..f690317b83 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -277,15 +277,17 @@ std::set Amd64OnlyConverter::_collectBranchTargets( std::set targets; for (const auto& match : matches) { const auto info = decoder.DecodeInstruction(seg.data() + match.Offset, match.Length); - const auto next = static_cast(ph.MappedAddress + match.Offset + info.Length); + const auto next = ph.MappedAddress + match.Offset + info.Length; if (info.HasBranchTarget && !info.HasRipRelativeDisp) { - const auto target = next + info.BranchDisp; - if (target >= 0 && !pastPrefixes(static_cast(target))) - targets.insert(static_cast(target)); + const auto displacement = static_cast(info.BranchDisp); + const auto target = next + displacement; + const auto wrapped = info.BranchDisp < 0 ? target > next : target < next; + if (!wrapped && !pastPrefixes(target)) + targets.insert(target); } else if (info.HasRipRelativeDisp && info.Opcode == kLea) { std::int32_t displacement; std::memcpy(&displacement, seg.data() + match.Offset + info.RipRelativeDispOffset, sizeof(displacement)); - const auto table = static_cast(next + displacement); + const auto table = next + static_cast(displacement); for (const auto& data : segments) { if (data.Type != kLoadSegment || table < data.MappedAddress || table - data.MappedAddress >= data.FileSize) continue; diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index ca2cf46715..5621d19922 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -520,6 +520,20 @@ void converterSegment() { requireFailure([&] { (void)converter->Convert(file, {segmentHeader(0x200)}); }, "Segment exceeding the file was accepted"); } +void converterSegmentMappedAtInt64Max() { + const auto converter = Codegen::MakeAmd64OnlyConverter(); + const auto file = segmentFixture(); + const Domain::ProgramHeader mappedAtInt64Max{1, 5, 0x200, 0x8000000000000000, 0, 20, 20, 16}; + const auto converted = converter->Convert(file, {mappedAtInt64Max}); + require(converted.ReplacedCount == 1 && converted.Trampolines.size() == 1, "Converter misclassified a segment mapped at 2^63"); + auto branchInto = file; + branchInto[0x207] = 0x02; + requireFailure([&] { (void)converter->Convert(branchInto, {mappedAtInt64Max}); }, "A branch into an AMD-only instruction was accepted in a segment mapped at 2^63"); + branchInto[0x207] = 0x80; + const auto backward = converter->Convert(branchInto, {mappedAtInt64Max}); + require(backward.ReplacedCount == 1 && backward.Trampolines.size() == 1, "A backward branch from a segment mapped at 2^63 changed the conversion"); +} + void converterRipRelativeFollower() { const auto converter = Codegen::MakeAmd64OnlyConverter(); const auto withFollower = [](const Bytes& following) { @@ -2151,6 +2165,7 @@ int main() { rdpruExecution(); mcommitExecution(); converterSegment(); + converterSegmentMappedAtInt64Max(); converterRipRelativeFollower(); converterSha256(); converterSha1(); From 05bd732eaf2418deb8bc398a716bc76ce85885ef Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 20:38:30 +0200 Subject: [PATCH 034/702] test(tools): tighten inner ELF version regressions --- tools/tests/test_unwrap_self.py | 54 ++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 38edec0c90..99fc352fea 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -672,38 +672,38 @@ class UnwrapTest(unittest.TestCase): container = bytearray(build_container(default_segments(), default_records(), default_payloads())) container[ELF_HEADER_OFFSET + 6] = identification struct.pack_into(" Date: Sat, 24 Oct 2026 21:33:51 +0200 Subject: [PATCH 035/702] fix(tools): identify JSON report recursion errors --- tools/performance_compare.py | 8 +++++++- tools/tests/test_performance_compare.py | 17 +++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/tools/performance_compare.py b/tools/performance_compare.py index 429a0858ba..07f21ebe3d 100644 --- a/tools/performance_compare.py +++ b/tools/performance_compare.py @@ -181,7 +181,13 @@ def main(argv=None): report = compare(load_summary(args.baseline, args.baseline_trace), load_summary(args.candidate, args.candidate_trace)) if args.require_matching_context and report["configuration_differences"]: raise ValueError("recorded configuration differs: " + ", ".join(report["configuration_differences"])) - output = json.dumps(report, indent=2, ensure_ascii=False, allow_nan=False) + "\n" if args.json else markdown(report) + if args.json: + try: + output = json.dumps(report, indent=2, ensure_ascii=False, allow_nan=False) + "\n" + except RecursionError as error: + raise ValueError(f"JSON report: {error}") from error + else: + output = markdown(report) except (OSError, ValueError, OverflowError, RecursionError) as error: parser.exit(2, f"{parser.prog}: {error}\n") sys.stdout.write(output) diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py index 67781ba46b..b40de66ed3 100644 --- a/tools/tests/test_performance_compare.py +++ b/tools/tests/test_performance_compare.py @@ -32,7 +32,7 @@ IDENTITY = {"cpu": 'CPU "X"', "cpu_threads": 16, "memory_mib": 32000, @lru_cache(maxsize=2) def excessive_json_value(opening, closing): depth = sys.getrecursionlimit() + 64 - for attempt in range(8): + for _ in range(8): value = opening * depth + "0" + closing * depth try: json.loads(value) @@ -228,7 +228,8 @@ class PerformanceCompareTests(unittest.TestCase): performance_compare.main([str(self.before), str(self.after), "--json"]) self.assertEqual(error.exception.code, 2) self.assertEqual(output.getvalue(), "") - self.assertIn("report nesting is too deep", errors.getvalue()) + self.assertIn("JSON report: report nesting is too deep", errors.getvalue()) + self.assertNotIn("Traceback", errors.getvalue()) def test_supported_nested_metadata_is_preserved(self): metadata = {"value": 7} @@ -237,15 +238,11 @@ class PerformanceCompareTests(unittest.TestCase): self.baseline["extra"] = metadata self.write(self.before, self.baseline) self.write(self.after, self.candidate) - for args in ((), ("--json",)): - with self.subTest(args=args), patch.object(sys, "stdout", new_callable=io.StringIO) as output, \ - patch.object(sys, "stderr", new_callable=io.StringIO) as errors: - performance_compare.main([str(self.before), str(self.after), *args]) + with patch.object(sys, "stdout", new_callable=io.StringIO) as output, \ + patch.object(sys, "stderr", new_callable=io.StringIO) as errors: + performance_compare.main([str(self.before), str(self.after), "--json"]) self.assertEqual(errors.getvalue(), "") - if args: - self.assertEqual(json.loads(output.getvalue())["baseline"]["extra"], metadata) - else: - self.assertIn("# Performance comparison", output.getvalue()) + self.assertEqual(json.loads(output.getvalue())["baseline"]["extra"], metadata) def test_markdown_reports_deltas_and_escapes_table_values(self): self.candidate["frame_ms"]["max"] = 60.0 From eaeb9c7de7ad7782c1aa0e8559af234bf9485afb Mon Sep 17 00:00:00 2001 From: dwk001 Date: Sat, 24 Oct 2026 19:41:49 +0000 Subject: [PATCH 036/702] fix(libkernel): accept any pthread_mutexattr_setkind_np kind as FreeBSD libthr does pthread_mutexattr_setkind_np stored only kinds 1 to 4 and threw for others. FreeBSD 11.4 stores any kind, pthread_mutexattr_gettype rejects kinds of PTHREAD_MUTEX_TYPE_MAX or more with EINVAL, and mutex_check_attr makes pthread_mutex_init fail with EINVAL for a kind outside the mutex types. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B7MJ4KkM4JNNWQxjsXT4ib --- core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp | 3 +-- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 2 ++ core/libs/tests/GuestPthreadMutexattr.cpp | 13 ++++++++++--- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index fbf631b696..d0eadf7003 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -166,6 +166,7 @@ int APS5_VABI pthread_mutexattr_settype_nid_postfix(PthreadMutexattr* attr, int int APS5_VABI pthread_mutexattr_gettype_nid_postfix(const PthreadMutexattr* attr, int* type) { if (!attr || !*attr) return POSIX_EINVAL; if (!type) throw std::runtime_error("pthread_mutexattr_gettype: null type"); + if (static_cast((*attr)->type) > static_cast(MutexType::Adaptive)) return POSIX_EINVAL; *type = static_cast((*attr)->type); return 0; } @@ -175,8 +176,6 @@ int APS5_VABI pthread_mutexattr_setkind_np_nid_postfix(PthreadMutexattr* attr, i *__error_nid_postfix() = POSIX_EINVAL; return -1; } - if (kind < static_cast(MutexType::ErrorCheck) || kind > static_cast(MutexType::Adaptive)) - throw std::runtime_error("pthread_mutexattr_setkind_np: kind " + std::to_string(kind) + " is not a mutex type, and pthread_mutex_init does not reject it with EINVAL"); (*attr)->type = static_cast(kind); return 0; } diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 6e8cc3df59..ecffd8abfb 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -152,6 +152,8 @@ int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char*) { if (!mutex) throw std::invalid_argument("Mutex pointer is null"); + if (attr && *attr && ((*attr)->type < MutexType::ErrorCheck || (*attr)->type > MutexType::Adaptive)) + return SCE_KERNEL_ERROR_EINVAL; auto replacement = std::make_unique(); if (attr && *attr) replacement->SetType((*attr)->type); diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 859dc770e1..73e1e28a97 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -113,10 +113,17 @@ static void CheckKind() { } Require(pthread_mutexattr_settype_nid_postfix(&attr, 2) == 0); Require(pthread_mutexattr_getkind_np_nid_postfix(attr) == 2); - for (const int invalid : {0, 5, -1}) - Require(Throws([&] { pthread_mutexattr_setkind_np_nid_postfix(&attr, invalid); })); - Require(pthread_mutexattr_getkind_np_nid_postfix(attr) == 2); PthreadMutex mutex = nullptr; + Require(pthread_mutexattr_setkind_np_nid_postfix(&attr, 0) == 0); + Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 0); + for (const int invalid : {0, 5, -1}) { + Require(pthread_mutexattr_setkind_np_nid_postfix(&attr, invalid) == 0); + Require(pthread_mutexattr_getkind_np_nid_postfix(attr) == invalid); + type = -1; + if (invalid != 0) Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == GUEST_EINVAL && type == -1); + Require(pthread_mutex_init_nid_postfix(&mutex, &attr) == GUEST_EINVAL && mutex == nullptr); + } + Require(pthread_mutexattr_setkind_np_nid_postfix(&attr, 2) == 0); Require(pthread_mutex_init_nid_postfix(&mutex, &attr) == 0); Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index ead756f295..96d22d315c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -264,9 +264,9 @@ - [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same - [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports the host volume of a file, not a console mount: block sizes, block counts, name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0). The type, mount names, owner, filesystem id and I/O counters are 0 or empty. Sockets, pipes and kqueues return `EINVAL`, as FreeBSD's `getvnode` does. A character device, including `/dev/random` and `/dev/urandom`, reports FreeBSD 11.4's [`devfs_statfs`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/fs/devfs/devfs_vfsops.c#L225-L237) values on every host: no flags, 512-byte blocks, 2 blocks, nothing free and a 255-byte name limit. A Windows descriptor that is none of these throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever +- [pthread_mutexattr_setkind_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) stores any kind, as FreeBSD 11.4's [`_pthread_mutexattr_setkind_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutexattr.c#L108-L120) does; `pthread_mutexattr_gettype` then fails with `EINVAL` for a kind of 5 or more (negative kinds included, as the unsigned type), and `pthread_mutex_init` and `scePthreadMutexInit` fail with `EINVAL` for a kind outside 1 to 4, as [`mutex_check_attr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L207-L218) does. Not measured on a PS5 - [pthread_mutex_getspinloops_np, pthread_mutex_setspinloops_np, pthread_mutex_getyieldloops_np, pthread_mutex_setyieldloops_np and their scePthreadMutex counterparts](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) store and report the counts, but host mutexes do not spin or yield by them. The defaults follow FreeBSD 11.4's [`mutex_init_body`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L236-L272) without `LIBPTHREAD_SPINLOOPS` and `LIBPTHREAD_YIELDLOOPS`: 2000 spins for an adaptive mutex, 0 otherwise; the console's defaults are unverified. Setting the yield count of a destroyed mutex returns 0 as FreeBSD's [`__pthread_mutex_setyieldloops_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L1125-L1135) does; the console's result is unverified - [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 -- [pthread_mutexattr_setkind_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) throws for a kind outside 1 to 4. FreeBSD 11.4's [`_pthread_mutexattr_setkind_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutexattr.c#L108-L120) stores any kind and [`mutex_check_attr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L207-L218) then makes `pthread_mutex_init` fail with `EINVAL`, but `pthread_mutex_init` here does not check the type - [pthread_cond_signalto_np, scePthreadCondSignalto](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) wake every waiter of the condition variable, not only the given thread, and do not check the thread; the other waiters see a spurious wakeup. Which waiters the console wakes, and its result for a thread that is not waiting, are unverified - [pthread_rwlockattr_gettype_np, scePthreadRwlockattrGettype](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) report the type stored by `scePthreadRwlockattrSettype`, which accepts any value and which read-write locks do not apply; the console's valid types and their effect are unverified. A null or uninitialized attribute throws, as for `pthread_rwlockattr_getpshared` - [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) throws for `fflags` above 0xFFFF, where OR and addition of 0x80020000 differ and the console was not measured. Otherwise it returns 0 for `fflags` 0 and `0x80020000 | fflags`, whatever `flags` and `data` hold, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) does and a PS5 (firmware 13.40, measured 2026-10-19 in a payload process) answered: `{flags 0x4000, fflags 0, data 22}`, `{0x4000, 0, -1}`, `{0x4000, 0, 0x80020016}` and `{0, 0, 0}` gave 0, `{0x4000, 5, 0}` and `{0x4000, 5, 22}` 0x80020005, `{0, 7, 22}` 0x80020007, `{0x8000, 3, 9}` 0x80020003. Video-out events carry their count in `fflags`, so they get 0x80020001 and up; only events built by the caller were measured. Closes when: `fflags` above 0xFFFF is measured From 4d8f05891bc4e30f835f33fbc2ff4dc0dab3053d Mon Sep 17 00:00:00 2001 From: Henryisdjfunkyho <197676650+Henryisdjfunkyho@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:53:51 -0700 Subject: [PATCH 037/702] test(libkernel): restore synchronized SCE sleep cancellation coverage --- core/libs/prx/libkernel/CMakeLists.txt | 11 ++++++++ core/libs/prx/libkernel/Time/TimedWait.cpp | 5 ++++ core/libs/tests/GuestSleepCancel.cpp | 33 +++++++++++++++------- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 5e507ed890..e12d5a987f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -2052,4 +2052,15 @@ if(BUILD_TESTING) endif() endforeach() endforeach() + foreach(sleepApi IN ITEMS sce_usleep sce_nanosleep sce_sleep) + set(sleepTest guest_sleep_cancel_${sleepApi}_deferred) + add_test(NAME ${sleepTest} COMMAND guest_sleep_cancel_tests deferred ${sleepApi}) + add_test(NAME ${sleepTest}_coarse COMMAND guest_sleep_cancel_tests deferred ${sleepApi}) + set_tests_properties(${sleepTest} ${sleepTest}_coarse PROPERTIES TIMEOUT 20) + set_tests_properties(${sleepTest}_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + if(WIN32) + set_tests_properties(${sleepTest} ${sleepTest}_coarse PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index bb5ba2cdca..b60cfb995d 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -619,6 +619,11 @@ void SleepNanos(std::uint64_t nanos) { } SleepUntil(NowNanos() + nanos); #else + struct SleepRegistration { + std::atomic* state = WaitState().bound; + SleepRegistration() { state->fetch_add(1, std::memory_order_seq_cst); } + ~SleepRegistration() { state->fetch_sub(1, std::memory_order_seq_cst); } + } registration; struct timespec req{}; req.tv_sec = static_cast(nanos / 1000000000ULL); req.tv_nsec = static_cast(nanos % 1000000000ULL); diff --git a/core/libs/tests/GuestSleepCancel.cpp b/core/libs/tests/GuestSleepCancel.cpp index 3b3ea39e9f..11daf21aac 100644 --- a/core/libs/tests/GuestSleepCancel.cpp +++ b/core/libs/tests/GuestSleepCancel.cpp @@ -39,9 +39,11 @@ static bool pending = false; static unsigned seconds = LongSleep; static std::string_view api = "sleep"; static unsigned cleanupCalls = 0; +static std::chrono::steady_clock::time_point sleepStart{}; static std::chrono::steady_clock::duration elapsed{}; static void APS5_VABI Cleanup(void*) { + elapsed = std::chrono::steady_clock::now() - sleepStart; ++cleanupCalls; } @@ -64,9 +66,9 @@ static void* APS5_VABI Sleeper(void*) { ready.store(true); ready.notify_all(); if (pending) release.wait(false); - const auto start = std::chrono::steady_clock::now(); + sleepStart = std::chrono::steady_clock::now(); Require(Sleep() == 0); - elapsed = std::chrono::steady_clock::now() - start; + elapsed = std::chrono::steady_clock::now() - sleepStart; returned.store(true); if (disabled) { Require(scePthreadSetcancelstate(0, nullptr) == 0); @@ -86,13 +88,15 @@ static bool WaitForCompletion(Pthread thread) { return true; } -static bool WaitForSleep(Pthread thread) { +static bool WaitForSleep(Pthread thread, bool hostSleep) { const auto limit = std::chrono::steady_clock::now() + ObservationLimit; for (;;) { { std::lock_guard lock(thread->cancelLock); if (thread->cancelWait != nullptr) return true; } + if (hostSleep && thread->waitCount.load(std::memory_order_seq_cst) > 0) return true; + if (thread->_finished.load(std::memory_order_acquire)) return false; if (std::chrono::steady_clock::now() >= limit) return false; std::this_thread::yield(); } @@ -106,24 +110,33 @@ int main(int count, char** arguments) { const bool zero = mode == "pending"; pending = zero || disabled || mode == "pending_long"; const bool normal = mode == "normal"; - Require(mode == "blocked" || disabled || pending || normal); - if (disabled || normal) seconds = 1; + const bool deferred = mode == "deferred"; + Require(mode == "blocked" || disabled || pending || normal || deferred); + Require(!deferred || api == "sce_usleep" || api == "sce_nanosleep" || api == "sce_sleep"); + if (disabled || normal || deferred) seconds = 1; if (zero) seconds = 0; Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Sleeper, nullptr, nullptr) == 0); - ready.wait(false); + const auto readyLimit = std::chrono::steady_clock::now() + ObservationLimit; + while (!ready.load()) { + Require(std::chrono::steady_clock::now() < readyLimit); + std::this_thread::yield(); + } bool wasSleeping = false; if (!normal) { - if (!pending) wasSleeping = WaitForSleep(thread); + if (!pending) { + wasSleeping = WaitForSleep(thread, deferred); + Require(wasSleeping); + } Require(scePthreadCancel(thread) == 0); release.store(true); release.notify_all(); } const bool completed = WaitForCompletion(thread); - void* result = nullptr; - Require(scePthreadJoin(thread, &result) == 0); if (!completed) std::fputs("sleep stayed blocked after cancellation\n", stderr); Require(completed); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == 0); if (normal) { Require(result == nullptr && returned.load()); Require(cleanupCalls == 0); @@ -133,5 +146,5 @@ int main(int count, char** arguments) { Require(returned.load() == disabled); if (!pending) Require(wasSleeping); } - if (normal || disabled) Require(elapsed >= std::chrono::seconds(1)); + if (normal || disabled || deferred) Require(elapsed >= std::chrono::seconds(1)); } From f86453bc01f4e6e614184fa269b69ce76d2e0ad7 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:32:17 +0100 Subject: [PATCH 038/702] feat(libSceFreeTypeOl): declare 150 PS5 3.20 exports --- core/libs/prx/libSceFreeTypeOl/Export.cpp | 751 +++++++++++++++++++++- docs/dev/debt/libSceFreeTypeOl.md | 5 + 2 files changed, 755 insertions(+), 1 deletion(-) create mode 100644 docs/dev/debt/libSceFreeTypeOl.md diff --git a/core/libs/prx/libSceFreeTypeOl/Export.cpp b/core/libs/prx/libSceFreeTypeOl/Export.cpp index d81f4a5e32..ad7b530ad4 100644 --- a/core/libs/prx/libSceFreeTypeOl/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOl/Export.cpp @@ -5,6 +5,755 @@ extern "C" { -APS5_DUMMY_FUN + +int APS5_VABI sceFreeTypeOlUnknown_mCIXittfvrw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mCIXittfvrw", sceFreeTypeOlUnknown_mCIXittfvrw_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_r2C1PcTDYxk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("r2C1PcTDYxk", sceFreeTypeOlUnknown_r2C1PcTDYxk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_1gAJb1uKyGo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1gAJb1uKyGo", sceFreeTypeOlUnknown_1gAJb1uKyGo_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_IjquEAbfizc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("IjquEAbfizc", sceFreeTypeOlUnknown_IjquEAbfizc_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_L_qInx9Dldk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("L-qInx9Dldk", sceFreeTypeOlUnknown_L_qInx9Dldk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_mhK_E1i3VuE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mhK+E1i3VuE", sceFreeTypeOlUnknown_mhK_E1i3VuE_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_bAbUJvsiVdI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bAbUJvsiVdI", sceFreeTypeOlUnknown_bAbUJvsiVdI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_1GYsjUC_HLM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1GYsjUC-HLM", sceFreeTypeOlUnknown_1GYsjUC_HLM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_KLaWCpRxYWo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KLaWCpRxYWo", sceFreeTypeOlUnknown_KLaWCpRxYWo_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_qodr9TJuFds_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qodr9TJuFds", sceFreeTypeOlUnknown_qodr9TJuFds_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_Ic8JMhUFe88_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Ic8JMhUFe88", sceFreeTypeOlUnknown_Ic8JMhUFe88_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_CIrV8Sq2Uic_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("CIrV8Sq2Uic", sceFreeTypeOlUnknown_CIrV8Sq2Uic_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_wbyY8kfNfaI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wbyY8kfNfaI", sceFreeTypeOlUnknown_wbyY8kfNfaI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_eS_4GhL28gs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("eS+4GhL28gs", sceFreeTypeOlUnknown_eS_4GhL28gs_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_XpA3dnvjl8w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XpA3dnvjl8w", sceFreeTypeOlUnknown_XpA3dnvjl8w_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_NLA7bmDS1jc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NLA7bmDS1jc", sceFreeTypeOlUnknown_NLA7bmDS1jc_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_xcJCXHqBrjE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xcJCXHqBrjE", sceFreeTypeOlUnknown_xcJCXHqBrjE_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_oKG_LjaBzzs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("oKG-LjaBzzs", sceFreeTypeOlUnknown_oKG_LjaBzzs_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_zxsyxyiLthM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zxsyxyiLthM", sceFreeTypeOlUnknown_zxsyxyiLthM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown__v66TBg7tzA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+v66TBg7tzA", sceFreeTypeOlUnknown__v66TBg7tzA_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_ZYpIYZg3Jf4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZYpIYZg3Jf4", sceFreeTypeOlUnknown_ZYpIYZg3Jf4_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_JSi_hWkA9pI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JSi+hWkA9pI", sceFreeTypeOlUnknown_JSi_hWkA9pI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_bZYqFlUVPCY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bZYqFlUVPCY", sceFreeTypeOlUnknown_bZYqFlUVPCY_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_ZhVdSRyrpUI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZhVdSRyrpUI", sceFreeTypeOlUnknown_ZhVdSRyrpUI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_phi4au_2t04_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("phi4au-2t04", sceFreeTypeOlUnknown_phi4au_2t04_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_qC_tiGwcW7E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qC+tiGwcW7E", sceFreeTypeOlUnknown_qC_tiGwcW7E_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_rl7aW17qQb8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("rl7aW17qQb8", sceFreeTypeOlUnknown_rl7aW17qQb8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown__qwk1RxUuDA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-qwk1RxUuDA", sceFreeTypeOlUnknown__qwk1RxUuDA_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_mQYQAi8B0Yo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mQYQAi8B0Yo", sceFreeTypeOlUnknown_mQYQAi8B0Yo_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_4TJ1qJGXAV8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4TJ1qJGXAV8", sceFreeTypeOlUnknown_4TJ1qJGXAV8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_IewcaRR3F8I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("IewcaRR3F8I", sceFreeTypeOlUnknown_IewcaRR3F8I_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_vuuFugcw5ss_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("vuuFugcw5ss", sceFreeTypeOlUnknown_vuuFugcw5ss_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_xaswa_ndZS0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xaswa+ndZS0", sceFreeTypeOlUnknown_xaswa_ndZS0_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_6zDeyzjWz_8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6zDeyzjWz-8", sceFreeTypeOlUnknown_6zDeyzjWz_8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_5_z1quqcDYo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5-z1quqcDYo", sceFreeTypeOlUnknown_5_z1quqcDYo_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_VVfxa65sWF0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("VVfxa65sWF0", sceFreeTypeOlUnknown_VVfxa65sWF0_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown__bRRhPeBqZI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+bRRhPeBqZI", sceFreeTypeOlUnknown__bRRhPeBqZI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_q7kTOFaQrW4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("q7kTOFaQrW4", sceFreeTypeOlUnknown_q7kTOFaQrW4_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_aX8PI_h3_dE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("aX8PI-h3+dE", sceFreeTypeOlUnknown_aX8PI_h3_dE_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_KIKL0rz6o_A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KIKL0rz6o-A", sceFreeTypeOlUnknown_KIKL0rz6o_A_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_QPh_yb2Y8AQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QPh+yb2Y8AQ", sceFreeTypeOlUnknown_QPh_yb2Y8AQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_u36SHyuWQCA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("u36SHyuWQCA", sceFreeTypeOlUnknown_u36SHyuWQCA_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_ZHsmiChQLTk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZHsmiChQLTk", sceFreeTypeOlUnknown_ZHsmiChQLTk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_q2ESpjCIk7w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("q2ESpjCIk7w", sceFreeTypeOlUnknown_q2ESpjCIk7w_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_C0nVvG6_x_s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("C0nVvG6+x-s", sceFreeTypeOlUnknown_C0nVvG6_x_s_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_DIIRFBxRfMQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DIIRFBxRfMQ", sceFreeTypeOlUnknown_DIIRFBxRfMQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_s3qCnsS3ATE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("s3qCnsS3ATE", sceFreeTypeOlUnknown_s3qCnsS3ATE_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_OutzOzZp6_w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OutzOzZp6-w", sceFreeTypeOlUnknown_OutzOzZp6_w_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_E9y7_AmuziU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("E9y7-AmuziU", sceFreeTypeOlUnknown_E9y7_AmuziU_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown__whG27_m_C0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-whG27+m+C0", sceFreeTypeOlUnknown__whG27_m_C0_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_wGGhfabJAbs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wGGhfabJAbs", sceFreeTypeOlUnknown_wGGhfabJAbs_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_KuJkc0qDvOQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KuJkc0qDvOQ", sceFreeTypeOlUnknown_KuJkc0qDvOQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_jqMurvieLBk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jqMurvieLBk", sceFreeTypeOlUnknown_jqMurvieLBk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_z9YQIyuTMV4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("z9YQIyuTMV4", sceFreeTypeOlUnknown_z9YQIyuTMV4_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_SKUldaN3e2A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SKUldaN3e2A", sceFreeTypeOlUnknown_SKUldaN3e2A_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_50HTRXvbW_k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("50HTRXvbW-k", sceFreeTypeOlUnknown_50HTRXvbW_k_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_M5dtVv0ln5Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("M5dtVv0ln5Q", sceFreeTypeOlUnknown_M5dtVv0ln5Q_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_xtb9fOR6ilA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xtb9fOR6ilA", sceFreeTypeOlUnknown_xtb9fOR6ilA_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_6z2P2V2XCiI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6z2P2V2XCiI", sceFreeTypeOlUnknown_6z2P2V2XCiI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_MJVrwM6aTTk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("MJVrwM6aTTk", sceFreeTypeOlUnknown_MJVrwM6aTTk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_EveUtaCYQgk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EveUtaCYQgk", sceFreeTypeOlUnknown_EveUtaCYQgk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_B7Sw_iUs7e4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("B7Sw-iUs7e4", sceFreeTypeOlUnknown_B7Sw_iUs7e4_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_RM21Fgm0ZvQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RM21Fgm0ZvQ", sceFreeTypeOlUnknown_RM21Fgm0ZvQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_OmxJF0FvwWc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OmxJF0FvwWc", sceFreeTypeOlUnknown_OmxJF0FvwWc_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_4to3tThESn0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4to3tThESn0", sceFreeTypeOlUnknown_4to3tThESn0_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_JuzrPZClLew_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JuzrPZClLew", sceFreeTypeOlUnknown_JuzrPZClLew_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_sylevnx_fqk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sylevnx-fqk", sceFreeTypeOlUnknown_sylevnx_fqk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_0RSuGozMl9o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0RSuGozMl9o", sceFreeTypeOlUnknown_0RSuGozMl9o_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_J3HnvW8FJV8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("J3HnvW8FJV8", sceFreeTypeOlUnknown_J3HnvW8FJV8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_2D3H_1nhwqQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2D3H+1nhwqQ", sceFreeTypeOlUnknown_2D3H_1nhwqQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_upeKra1FaQk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("upeKra1FaQk", sceFreeTypeOlUnknown_upeKra1FaQk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_YpxR8CqGMIM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("YpxR8CqGMIM", sceFreeTypeOlUnknown_YpxR8CqGMIM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_83qtpn8_XnA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("83qtpn8+XnA", sceFreeTypeOlUnknown_83qtpn8_XnA_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_g2p7fkJpuC0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("g2p7fkJpuC0", sceFreeTypeOlUnknown_g2p7fkJpuC0_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_xqjAbn8VNGo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xqjAbn8VNGo", sceFreeTypeOlUnknown_xqjAbn8VNGo_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_Yu7mmGCjSYM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Yu7mmGCjSYM", sceFreeTypeOlUnknown_Yu7mmGCjSYM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_SDPzjFN7y4E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SDPzjFN7y4E", sceFreeTypeOlUnknown_SDPzjFN7y4E_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_oM0zMNCfXKM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("oM0zMNCfXKM", sceFreeTypeOlUnknown_oM0zMNCfXKM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_3dTNNBoujn0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3dTNNBoujn0", sceFreeTypeOlUnknown_3dTNNBoujn0_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_Tzm_z36UYKg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Tzm-z36UYKg", sceFreeTypeOlUnknown_Tzm_z36UYKg_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_b7S96aD8dkE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("b7S96aD8dkE", sceFreeTypeOlUnknown_b7S96aD8dkE_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_M94SjXo_UVA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("M94SjXo-UVA", sceFreeTypeOlUnknown_M94SjXo_UVA_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_Ns9a_LjOcDw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Ns9a-LjOcDw", sceFreeTypeOlUnknown_Ns9a_LjOcDw_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_EBvr2CZnjVo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EBvr2CZnjVo", sceFreeTypeOlUnknown_EBvr2CZnjVo_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_pSgOzwkzx4Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pSgOzwkzx4Y", sceFreeTypeOlUnknown_pSgOzwkzx4Y_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_LonHHN98tMI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LonHHN98tMI", sceFreeTypeOlUnknown_LonHHN98tMI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_Uw_BZJpflxk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Uw+BZJpflxk", sceFreeTypeOlUnknown_Uw_BZJpflxk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_D1l8_Bu__Y8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("D1l8+Bu+-Y8", sceFreeTypeOlUnknown_D1l8_Bu__Y8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_p90G4PB_rPc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("p90G4PB-rPc", sceFreeTypeOlUnknown_p90G4PB_rPc_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_21vkVuOP2lk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("21vkVuOP2lk", sceFreeTypeOlUnknown_21vkVuOP2lk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_ZqvTX8u1bEc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZqvTX8u1bEc", sceFreeTypeOlUnknown_ZqvTX8u1bEc_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_cR_yQldyXvs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cR-yQldyXvs", sceFreeTypeOlUnknown_cR_yQldyXvs_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_1GL9pCFuiHQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1GL9pCFuiHQ", sceFreeTypeOlUnknown_1GL9pCFuiHQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_Mu2upxmY2C8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Mu2upxmY2C8", sceFreeTypeOlUnknown_Mu2upxmY2C8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_wcMdf_kMx7o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wcMdf-kMx7o", sceFreeTypeOlUnknown_wcMdf_kMx7o_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown__AeE58GkM_Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-AeE58GkM+Q", sceFreeTypeOlUnknown__AeE58GkM_Q_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_h0n4Fb9j2R8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("h0n4Fb9j2R8", sceFreeTypeOlUnknown_h0n4Fb9j2R8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_wLqQQWyOdpc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wLqQQWyOdpc", sceFreeTypeOlUnknown_wLqQQWyOdpc_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_92e4jZiW9Tc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("92e4jZiW9Tc", sceFreeTypeOlUnknown_92e4jZiW9Tc_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_exDiYq9Q1GM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("exDiYq9Q1GM", sceFreeTypeOlUnknown_exDiYq9Q1GM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_pO0sA7m_UKY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pO0sA7m+UKY", sceFreeTypeOlUnknown_pO0sA7m_UKY_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_yD4_QaNSink_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yD4-QaNSink", sceFreeTypeOlUnknown_yD4_QaNSink_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_fp_jO_c8Rfk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fp-jO+c8Rfk", sceFreeTypeOlUnknown_fp_jO_c8Rfk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_OgFczHko0w8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OgFczHko0w8", sceFreeTypeOlUnknown_OgFczHko0w8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_dH02CpEEcYk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dH02CpEEcYk", sceFreeTypeOlUnknown_dH02CpEEcYk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_RdrrejUMiJ0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RdrrejUMiJ0", sceFreeTypeOlUnknown_RdrrejUMiJ0_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_oJm1lThZ3iQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("oJm1lThZ3iQ", sceFreeTypeOlUnknown_oJm1lThZ3iQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_1XCz6Gk0Lt4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1XCz6Gk0Lt4", sceFreeTypeOlUnknown_1XCz6Gk0Lt4_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_UTumVjeIQtg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UTumVjeIQtg", sceFreeTypeOlUnknown_UTumVjeIQtg_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown__2rnxRCeHMs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-2rnxRCeHMs", sceFreeTypeOlUnknown__2rnxRCeHMs_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown__G9vZcqBAfY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+G9vZcqBAfY", sceFreeTypeOlUnknown__G9vZcqBAfY_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_dPcZ76eZb64_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dPcZ76eZb64", sceFreeTypeOlUnknown_dPcZ76eZb64_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_Vrab2WK1mgk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Vrab2WK1mgk", sceFreeTypeOlUnknown_Vrab2WK1mgk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_tl72PmqhXww_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tl72PmqhXww", sceFreeTypeOlUnknown_tl72PmqhXww_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_9Lm2mfsSmws_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("9Lm2mfsSmws", sceFreeTypeOlUnknown_9Lm2mfsSmws_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_mGMeRKWGTL4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mGMeRKWGTL4", sceFreeTypeOlUnknown_mGMeRKWGTL4_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_EruGr0TR1CU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EruGr0TR1CU", sceFreeTypeOlUnknown_EruGr0TR1CU_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_EqN_ED6P2FA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EqN+ED6P2FA", sceFreeTypeOlUnknown_EqN_ED6P2FA_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_fHMsRluEMfI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fHMsRluEMfI", sceFreeTypeOlUnknown_fHMsRluEMfI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_dMSDtQsYuJM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dMSDtQsYuJM", sceFreeTypeOlUnknown_dMSDtQsYuJM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_l55oKq6zmrA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("l55oKq6zmrA", sceFreeTypeOlUnknown_l55oKq6zmrA_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_NLwiYXdX5WU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NLwiYXdX5WU", sceFreeTypeOlUnknown_NLwiYXdX5WU_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_p_LFZYbDPDU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("p-LFZYbDPDU", sceFreeTypeOlUnknown_p_LFZYbDPDU_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_Pr3eI2uGgvU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Pr3eI2uGgvU", sceFreeTypeOlUnknown_Pr3eI2uGgvU_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_MXgrzq0nh1Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("MXgrzq0nh1Q", sceFreeTypeOlUnknown_MXgrzq0nh1Q_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_2UZg_FAym4c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2UZg+FAym4c", sceFreeTypeOlUnknown_2UZg_FAym4c_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_obnSzeb_KXk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("obnSzeb-KXk", sceFreeTypeOlUnknown_obnSzeb_KXk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_edmDMsPE7Zg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("edmDMsPE7Zg", sceFreeTypeOlUnknown_edmDMsPE7Zg_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_H4eyE4ez7O4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("H4eyE4ez7O4", sceFreeTypeOlUnknown_H4eyE4ez7O4_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_qoysbJFu5sI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qoysbJFu5sI", sceFreeTypeOlUnknown_qoysbJFu5sI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_l4azYkX86Ds_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("l4azYkX86Ds", sceFreeTypeOlUnknown_l4azYkX86Ds_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_eMKHL_Cj50k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("eMKHL+Cj50k", sceFreeTypeOlUnknown_eMKHL_Cj50k_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_tGmYdLIYT98_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tGmYdLIYT98", sceFreeTypeOlUnknown_tGmYdLIYT98_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_LPtlB241JSg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LPtlB241JSg", sceFreeTypeOlUnknown_LPtlB241JSg_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_qJFh32pZAJQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qJFh32pZAJQ", sceFreeTypeOlUnknown_qJFh32pZAJQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_swrIPzOxcmM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("swrIPzOxcmM", sceFreeTypeOlUnknown_swrIPzOxcmM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_5NYsP564wwY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5NYsP564wwY", sceFreeTypeOlUnknown_5NYsP564wwY_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_pORpGtdlfqQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pORpGtdlfqQ", sceFreeTypeOlUnknown_pORpGtdlfqQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_sZzliEQrICM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sZzliEQrICM", sceFreeTypeOlUnknown_sZzliEQrICM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_egPbre4QGBI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("egPbre4QGBI", sceFreeTypeOlUnknown_egPbre4QGBI_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_NVuvsPgTXto_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NVuvsPgTXto", sceFreeTypeOlUnknown_NVuvsPgTXto_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_qBohKqdkAqE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qBohKqdkAqE", sceFreeTypeOlUnknown_qBohKqdkAqE_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_OqaxEkRhr_8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OqaxEkRhr+8", sceFreeTypeOlUnknown_OqaxEkRhr_8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_JQyLCevN3aM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JQyLCevN3aM", sceFreeTypeOlUnknown_JQyLCevN3aM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown__yNEa4OFH1M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+yNEa4OFH1M", sceFreeTypeOlUnknown__yNEa4OFH1M_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_Ik17GQynFpw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Ik17GQynFpw", sceFreeTypeOlUnknown_Ik17GQynFpw_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_GVnNjpJWCXM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("GVnNjpJWCXM", sceFreeTypeOlUnknown_GVnNjpJWCXM_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_1wFNGIk3FR0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1wFNGIk3FR0", sceFreeTypeOlUnknown_1wFNGIk3FR0_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_B4_zPp0EATE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("B4-zPp0EATE", sceFreeTypeOlUnknown_B4_zPp0EATE_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_fbRv4M_XzL8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fbRv4M+XzL8", sceFreeTypeOlUnknown_fbRv4M_XzL8_nid_no_patch); } diff --git a/docs/dev/debt/libSceFreeTypeOl.md b/docs/dev/debt/libSceFreeTypeOl.md new file mode 100644 index 0000000000..3ceb7f2085 --- /dev/null +++ b/docs/dev/debt/libSceFreeTypeOl.md @@ -0,0 +1,5 @@ +# libSceFreeTypeOl technical debt + +### Unknown function info + +- The 150 `sceFreeTypeOlUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOl/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From 85698000b21bd1325e6564dfe80d6c7642629c5d Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:35:13 +0100 Subject: [PATCH 039/702] fix(libSceFreeTypeOl): avoid known export NIDs --- core/libs/prx/libSceFreeTypeOl/Export.cpp | 51 +++++++++++------------ 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOl/Export.cpp b/core/libs/prx/libSceFreeTypeOl/Export.cpp index ad7b530ad4..99d7406eca 100644 --- a/core/libs/prx/libSceFreeTypeOl/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOl/Export.cpp @@ -5,7 +5,6 @@ extern "C" { - int APS5_VABI sceFreeTypeOlUnknown_mCIXittfvrw_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -521,16 +520,6 @@ int APS5_VABI sceFreeTypeOlUnknown_fp_jO_c8Rfk_nid_no_patch() { } APS5_EXPORT("fp-jO+c8Rfk", sceFreeTypeOlUnknown_fp_jO_c8Rfk_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_OgFczHko0w8_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("OgFczHko0w8", sceFreeTypeOlUnknown_OgFczHko0w8_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_dH02CpEEcYk_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("dH02CpEEcYk", sceFreeTypeOlUnknown_dH02CpEEcYk_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_RdrrejUMiJ0_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -636,11 +625,6 @@ int APS5_VABI sceFreeTypeOlUnknown_2UZg_FAym4c_nid_no_patch() { } APS5_EXPORT("2UZg+FAym4c", sceFreeTypeOlUnknown_2UZg_FAym4c_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_obnSzeb_KXk_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("obnSzeb-KXk", sceFreeTypeOlUnknown_obnSzeb_KXk_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_edmDMsPE7Zg_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -651,11 +635,6 @@ int APS5_VABI sceFreeTypeOlUnknown_H4eyE4ez7O4_nid_no_patch() { } APS5_EXPORT("H4eyE4ez7O4", sceFreeTypeOlUnknown_H4eyE4ez7O4_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_qoysbJFu5sI_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("qoysbJFu5sI", sceFreeTypeOlUnknown_qoysbJFu5sI_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_l4azYkX86Ds_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -701,11 +680,6 @@ int APS5_VABI sceFreeTypeOlUnknown_sZzliEQrICM_nid_no_patch() { } APS5_EXPORT("sZzliEQrICM", sceFreeTypeOlUnknown_sZzliEQrICM_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_egPbre4QGBI_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("egPbre4QGBI", sceFreeTypeOlUnknown_egPbre4QGBI_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_NVuvsPgTXto_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -756,4 +730,29 @@ int APS5_VABI sceFreeTypeOlUnknown_fbRv4M_XzL8_nid_no_patch() { } APS5_EXPORT("fbRv4M+XzL8", sceFreeTypeOlUnknown_fbRv4M_XzL8_nid_no_patch); +int APS5_VABI sceFreeTypeOlUnknown_omDz0x8lyHk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("omDz0x8lyHk", sceFreeTypeOlUnknown_omDz0x8lyHk_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_vOhW2Wdz0t4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("vOhW2Wdz0t4", sceFreeTypeOlUnknown_vOhW2Wdz0t4_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_e2KoMPOAdZg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("e2KoMPOAdZg", sceFreeTypeOlUnknown_e2KoMPOAdZg_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_eSFTUAm1wf8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("eSFTUAm1wf8", sceFreeTypeOlUnknown_eSFTUAm1wf8_nid_no_patch); + +int APS5_VABI sceFreeTypeOlUnknown_aWMUKhiwUbk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("aWMUKhiwUbk", sceFreeTypeOlUnknown_aWMUKhiwUbk_nid_no_patch); + } From 111a07aa5a06a662c04dfaa1e5e4a4857c8c94d8 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:15:25 +0100 Subject: [PATCH 040/702] docs: distinguish known exports from unverified ABI --- docs/dev/debt/libSceFreeTypeOl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceFreeTypeOl.md b/docs/dev/debt/libSceFreeTypeOl.md index 3ceb7f2085..9cb669a12b 100644 --- a/docs/dev/debt/libSceFreeTypeOl.md +++ b/docs/dev/debt/libSceFreeTypeOl.md @@ -1,5 +1,5 @@ # libSceFreeTypeOl technical debt -### Unknown function info +### Known export names; ABI prototypes unverified - The 150 `sceFreeTypeOlUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOl/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From a1d91b53aa50846d05844d1e456b57b11fdc6fc9 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:01:05 +0100 Subject: [PATCH 041/702] feat(libSceJsc): declare pending exports --- core/libs/prx/libSceJsc/Export.cpp | 750 ++++++++++++++++++++++++++++- docs/dev/debt/libSceJsc.md | 5 + 2 files changed, 754 insertions(+), 1 deletion(-) create mode 100644 docs/dev/debt/libSceJsc.md diff --git a/core/libs/prx/libSceJsc/Export.cpp b/core/libs/prx/libSceJsc/Export.cpp index d81f4a5e32..ff026739d6 100644 --- a/core/libs/prx/libSceJsc/Export.cpp +++ b/core/libs/prx/libSceJsc/Export.cpp @@ -5,6 +5,754 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceJscUnknown__0qGCrVTCcs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0qGCrVTCcs", sceJscUnknown__0qGCrVTCcs_nid_no_patch); + +int APS5_VABI sceJscUnknown__14hrhyqXkw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+14hrhyqXkw", sceJscUnknown__14hrhyqXkw_nid_no_patch); + +int APS5_VABI sceJscUnknown__3jAVQxPMOc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+3jAVQxPMOc", sceJscUnknown__3jAVQxPMOc_nid_no_patch); + +int APS5_VABI sceJscUnknown__i_vXbqYQMo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+i+vXbqYQMo", sceJscUnknown__i_vXbqYQMo_nid_no_patch); + +int APS5_VABI sceJscUnknown__jBdpELLGNA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+jBdpELLGNA", sceJscUnknown__jBdpELLGNA_nid_no_patch); + +int APS5_VABI sceJscUnknown__tn6ODC5FvQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+tn6ODC5FvQ", sceJscUnknown__tn6ODC5FvQ_nid_no_patch); + +int APS5_VABI sceJscUnknown__tuS_migTvI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+tuS+migTvI", sceJscUnknown__tuS_migTvI_nid_no_patch); + +int APS5_VABI sceJscUnknown__uKktfh_zD0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+uKktfh+zD0", sceJscUnknown__uKktfh_zD0_nid_no_patch); + +int APS5_VABI sceJscUnknown__uKqBYnAudU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+uKqBYnAudU", sceJscUnknown__uKqBYnAudU_nid_no_patch); + +int APS5_VABI sceJscUnknown__ugzaqpDezk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ugzaqpDezk", sceJscUnknown__ugzaqpDezk_nid_no_patch); + +int APS5_VABI sceJscUnknown__wd7ITlDmEM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+wd7ITlDmEM", sceJscUnknown__wd7ITlDmEM_nid_no_patch); + +int APS5_VABI sceJscUnknown__ww_pcpOFNc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ww+pcpOFNc", sceJscUnknown__ww_pcpOFNc_nid_no_patch); + +int APS5_VABI sceJscUnknown__x_ywEbOrJo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+x-ywEbOrJo", sceJscUnknown__x_ywEbOrJo_nid_no_patch); + +int APS5_VABI sceJscUnknown__yRvWsESIkM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+yRvWsESIkM", sceJscUnknown__yRvWsESIkM_nid_no_patch); + +int APS5_VABI sceJscUnknown__ypzWWOJMwI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ypzWWOJMwI", sceJscUnknown__ypzWWOJMwI_nid_no_patch); + +int APS5_VABI sceJscUnknown__z3kfOo91Pg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+z3kfOo91Pg", sceJscUnknown__z3kfOo91Pg_nid_no_patch); + +int APS5_VABI sceJscUnknown__zL3nHNv5GU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+zL3nHNv5GU", sceJscUnknown__zL3nHNv5GU_nid_no_patch); + +int APS5_VABI sceJscUnknown__zPRztSZLTg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+zPRztSZLTg", sceJscUnknown__zPRztSZLTg_nid_no_patch); + +int APS5_VABI sceJscUnknown__09a6Ceb6Y0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-09a6Ceb6Y0", sceJscUnknown__09a6Ceb6Y0_nid_no_patch); + +int APS5_VABI sceJscUnknown__1Qgg3W9FVM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-1Qgg3W9FVM", sceJscUnknown__1Qgg3W9FVM_nid_no_patch); + +int APS5_VABI sceJscUnknown__3x7_wyZpEU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-3x7+wyZpEU", sceJscUnknown__3x7_wyZpEU_nid_no_patch); + +int APS5_VABI sceJscUnknown__4M0K8nGdfk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-4M0K8nGdfk", sceJscUnknown__4M0K8nGdfk_nid_no_patch); + +int APS5_VABI sceJscUnknown__4jsoWj40_g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-4jsoWj40-g", sceJscUnknown__4jsoWj40_g_nid_no_patch); + +int APS5_VABI sceJscUnknown__4sVkp5_H_M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-4sVkp5-H-M", sceJscUnknown__4sVkp5_H_M_nid_no_patch); + +int APS5_VABI sceJscUnknown__5budpPgP8o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-5budpPgP8o", sceJscUnknown__5budpPgP8o_nid_no_patch); + +int APS5_VABI sceJscUnknown__69D4t3LLHE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-69D4t3LLHE", sceJscUnknown__69D4t3LLHE_nid_no_patch); + +int APS5_VABI sceJscUnknown__7g_5LVkStM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-7g-5LVkStM", sceJscUnknown__7g_5LVkStM_nid_no_patch); + +int APS5_VABI sceJscUnknown__7u_WG3kX3U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-7u-WG3kX3U", sceJscUnknown__7u_WG3kX3U_nid_no_patch); + +int APS5_VABI sceJscUnknown__9SPyZbLwdc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-9SPyZbLwdc", sceJscUnknown__9SPyZbLwdc_nid_no_patch); + +int APS5_VABI sceJscUnknown__F3z0DF32Fw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-F3z0DF32Fw", sceJscUnknown__F3z0DF32Fw_nid_no_patch); + +int APS5_VABI sceJscUnknown__G1ivTvs2mU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-G1ivTvs2mU", sceJscUnknown__G1ivTvs2mU_nid_no_patch); + +int APS5_VABI sceJscUnknown__HYD6ysDUts_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-HYD6ysDUts", sceJscUnknown__HYD6ysDUts_nid_no_patch); + +int APS5_VABI sceJscUnknown__KKlTIIqZrk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-KKlTIIqZrk", sceJscUnknown__KKlTIIqZrk_nid_no_patch); + +int APS5_VABI sceJscUnknown__LSSffiQqPg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-LSSffiQqPg", sceJscUnknown__LSSffiQqPg_nid_no_patch); + +int APS5_VABI sceJscUnknown__MSv8GTlzww_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-MSv8GTlzww", sceJscUnknown__MSv8GTlzww_nid_no_patch); + +int APS5_VABI sceJscUnknown__MicizMIiag_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-MicizMIiag", sceJscUnknown__MicizMIiag_nid_no_patch); + +int APS5_VABI sceJscUnknown__MnzCChBb5c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-MnzCChBb5c", sceJscUnknown__MnzCChBb5c_nid_no_patch); + +int APS5_VABI sceJscUnknown__N79h7XJG8A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-N79h7XJG8A", sceJscUnknown__N79h7XJG8A_nid_no_patch); + +int APS5_VABI sceJscUnknown__NAYLFFi9n8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-NAYLFFi9n8", sceJscUnknown__NAYLFFi9n8_nid_no_patch); + +int APS5_VABI sceJscUnknown__NOpB57jCtM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-NOpB57jCtM", sceJscUnknown__NOpB57jCtM_nid_no_patch); + +int APS5_VABI sceJscUnknown__Ru0nzM2cJ4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-Ru0nzM2cJ4", sceJscUnknown__Ru0nzM2cJ4_nid_no_patch); + +int APS5_VABI sceJscUnknown__SS6hQt_3dQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-SS6hQt+3dQ", sceJscUnknown__SS6hQt_3dQ_nid_no_patch); + +int APS5_VABI sceJscUnknown__T_6YbvQ3W4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-T+6YbvQ3W4", sceJscUnknown__T_6YbvQ3W4_nid_no_patch); + +int APS5_VABI sceJscUnknown__UWXejaXycI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-UWXejaXycI", sceJscUnknown__UWXejaXycI_nid_no_patch); + +int APS5_VABI sceJscUnknown__W0cyz1wnug_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-W0cyz1wnug", sceJscUnknown__W0cyz1wnug_nid_no_patch); + +int APS5_VABI sceJscUnknown__W6XjwwK8Bo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-W6XjwwK8Bo", sceJscUnknown__W6XjwwK8Bo_nid_no_patch); + +int APS5_VABI sceJscUnknown__W9fbd6kB_8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-W9fbd6kB-8", sceJscUnknown__W9fbd6kB_8_nid_no_patch); + +int APS5_VABI sceJscUnknown__WAFtiljCdo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-WAFtiljCdo", sceJscUnknown__WAFtiljCdo_nid_no_patch); + +int APS5_VABI sceJscUnknown__WzxDM_yTwY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-WzxDM-yTwY", sceJscUnknown__WzxDM_yTwY_nid_no_patch); + +int APS5_VABI sceJscUnknown__XCn1Pwn7NU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-XCn1Pwn7NU", sceJscUnknown__XCn1Pwn7NU_nid_no_patch); + +int APS5_VABI sceJscUnknown__XHaGqZr_PE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-XHaGqZr+PE", sceJscUnknown__XHaGqZr_PE_nid_no_patch); + +int APS5_VABI sceJscUnknown__Xia6SzTPnQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-Xia6SzTPnQ", sceJscUnknown__Xia6SzTPnQ_nid_no_patch); + +int APS5_VABI sceJscUnknown__Y_WZBvAu3M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-Y-WZBvAu3M", sceJscUnknown__Y_WZBvAu3M_nid_no_patch); + +int APS5_VABI sceJscUnknown__YDX6TPLnro_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-YDX6TPLnro", sceJscUnknown__YDX6TPLnro_nid_no_patch); + +int APS5_VABI sceJscUnknown__am9z4xyweE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-am9z4xyweE", sceJscUnknown__am9z4xyweE_nid_no_patch); + +int APS5_VABI sceJscUnknown__e_i2gtmhdc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-e-i2gtmhdc", sceJscUnknown__e_i2gtmhdc_nid_no_patch); + +int APS5_VABI sceJscUnknown__eUZ8xel7b0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-eUZ8xel7b0", sceJscUnknown__eUZ8xel7b0_nid_no_patch); + +int APS5_VABI sceJscUnknown__fAFmad5Tnk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-fAFmad5Tnk", sceJscUnknown__fAFmad5Tnk_nid_no_patch); + +int APS5_VABI sceJscUnknown__fKQRtsYUSc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-fKQRtsYUSc", sceJscUnknown__fKQRtsYUSc_nid_no_patch); + +int APS5_VABI sceJscUnknown__fwHkWlm_wc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-fwHkWlm-wc", sceJscUnknown__fwHkWlm_wc_nid_no_patch); + +int APS5_VABI sceJscUnknown__g6RLhTGOeg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-g6RLhTGOeg", sceJscUnknown__g6RLhTGOeg_nid_no_patch); + +int APS5_VABI sceJscUnknown__gE3O_NCHS0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-gE3O+NCHS0", sceJscUnknown__gE3O_NCHS0_nid_no_patch); + +int APS5_VABI sceJscUnknown__hdGlIfGY5g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-hdGlIfGY5g", sceJscUnknown__hdGlIfGY5g_nid_no_patch); + +int APS5_VABI sceJscUnknown__hk9wmuusRU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-hk9wmuusRU", sceJscUnknown__hk9wmuusRU_nid_no_patch); + +int APS5_VABI sceJscUnknown__md9AgyWZkw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-md9AgyWZkw", sceJscUnknown__md9AgyWZkw_nid_no_patch); + +int APS5_VABI sceJscUnknown__n_GMwKBaD0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-n+GMwKBaD0", sceJscUnknown__n_GMwKBaD0_nid_no_patch); + +int APS5_VABI sceJscUnknown__n_x6ZBExPE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-n+x6ZBExPE", sceJscUnknown__n_x6ZBExPE_nid_no_patch); + +int APS5_VABI sceJscUnknown__oW_aAeCPs4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-oW-aAeCPs4", sceJscUnknown__oW_aAeCPs4_nid_no_patch); + +int APS5_VABI sceJscUnknown__qMXl9DNcac_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-qMXl9DNcac", sceJscUnknown__qMXl9DNcac_nid_no_patch); + +int APS5_VABI sceJscUnknown__qYqo3TC_8I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-qYqo3TC+8I", sceJscUnknown__qYqo3TC_8I_nid_no_patch); + +int APS5_VABI sceJscUnknown__rEvUxwc4xY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-rEvUxwc4xY", sceJscUnknown__rEvUxwc4xY_nid_no_patch); + +int APS5_VABI sceJscUnknown__rtzyTjX8AM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-rtzyTjX8AM", sceJscUnknown__rtzyTjX8AM_nid_no_patch); + +int APS5_VABI sceJscUnknown__s4y23rdI0Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-s4y23rdI0Y", sceJscUnknown__s4y23rdI0Y_nid_no_patch); + +int APS5_VABI sceJscUnknown__sQ3aB_rrVw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-sQ3aB-rrVw", sceJscUnknown__sQ3aB_rrVw_nid_no_patch); + +int APS5_VABI sceJscUnknown__sjd__qKqaU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-sjd++qKqaU", sceJscUnknown__sjd__qKqaU_nid_no_patch); + +int APS5_VABI sceJscUnknown__tPqziuAbt0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-tPqziuAbt0", sceJscUnknown__tPqziuAbt0_nid_no_patch); + +int APS5_VABI sceJscUnknown__uLGa2Nmdg8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-uLGa2Nmdg8", sceJscUnknown__uLGa2Nmdg8_nid_no_patch); + +int APS5_VABI sceJscUnknown__xFvxRPfTng_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-xFvxRPfTng", sceJscUnknown__xFvxRPfTng_nid_no_patch); + +int APS5_VABI sceJscUnknown__zEvjr6MBaE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-zEvjr6MBaE", sceJscUnknown__zEvjr6MBaE_nid_no_patch); + +int APS5_VABI sceJscUnknown__zr4F1xDgGA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-zr4F1xDgGA", sceJscUnknown__zr4F1xDgGA_nid_no_patch); + +int APS5_VABI sceJscUnknown_0_mWELhv81k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0-mWELhv81k", sceJscUnknown_0_mWELhv81k_nid_no_patch); + +int APS5_VABI sceJscUnknown_01IKkgXrFxs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("01IKkgXrFxs", sceJscUnknown_01IKkgXrFxs_nid_no_patch); + +int APS5_VABI sceJscUnknown_01nXitlGn8k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("01nXitlGn8k", sceJscUnknown_01nXitlGn8k_nid_no_patch); + +int APS5_VABI sceJscUnknown_04XvCW_DZys_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("04XvCW-DZys", sceJscUnknown_04XvCW_DZys_nid_no_patch); + +int APS5_VABI sceJscUnknown_0AEWR2mf0dA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0AEWR2mf0dA", sceJscUnknown_0AEWR2mf0dA_nid_no_patch); + +int APS5_VABI sceJscUnknown_0BNiqtvNqTA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0BNiqtvNqTA", sceJscUnknown_0BNiqtvNqTA_nid_no_patch); + +int APS5_VABI sceJscUnknown_0BU_NMgvLhU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0BU+NMgvLhU", sceJscUnknown_0BU_NMgvLhU_nid_no_patch); + +int APS5_VABI sceJscUnknown_0BtcoCh5nbg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0BtcoCh5nbg", sceJscUnknown_0BtcoCh5nbg_nid_no_patch); + +int APS5_VABI sceJscUnknown_0CB7Dz8pEuA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0CB7Dz8pEuA", sceJscUnknown_0CB7Dz8pEuA_nid_no_patch); + +int APS5_VABI sceJscUnknown_0CGMc99sJQc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0CGMc99sJQc", sceJscUnknown_0CGMc99sJQc_nid_no_patch); + +int APS5_VABI sceJscUnknown_0CnwqNph95c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0CnwqNph95c", sceJscUnknown_0CnwqNph95c_nid_no_patch); + +int APS5_VABI sceJscUnknown_0Cu5IQZsCWA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Cu5IQZsCWA", sceJscUnknown_0Cu5IQZsCWA_nid_no_patch); + +int APS5_VABI sceJscUnknown_0DY9Snf59Wk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0DY9Snf59Wk", sceJscUnknown_0DY9Snf59Wk_nid_no_patch); + +int APS5_VABI sceJscUnknown_0DkchTKnu8Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0DkchTKnu8Y", sceJscUnknown_0DkchTKnu8Y_nid_no_patch); + +int APS5_VABI sceJscUnknown_0Eqn40ngE5M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Eqn40ngE5M", sceJscUnknown_0Eqn40ngE5M_nid_no_patch); + +int APS5_VABI sceJscUnknown_0FlXpxjRjOA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0FlXpxjRjOA", sceJscUnknown_0FlXpxjRjOA_nid_no_patch); + +int APS5_VABI sceJscUnknown_0FmMcDDxnAY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0FmMcDDxnAY", sceJscUnknown_0FmMcDDxnAY_nid_no_patch); + +int APS5_VABI sceJscUnknown_0GxlvzlC15A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0GxlvzlC15A", sceJscUnknown_0GxlvzlC15A_nid_no_patch); + +int APS5_VABI sceJscUnknown_0HkylFfb_Ig_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0HkylFfb+Ig", sceJscUnknown_0HkylFfb_Ig_nid_no_patch); + +int APS5_VABI sceJscUnknown_0I6apGOYhtM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0I6apGOYhtM", sceJscUnknown_0I6apGOYhtM_nid_no_patch); + +int APS5_VABI sceJscUnknown_0ICMatq6_6g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0ICMatq6+6g", sceJscUnknown_0ICMatq6_6g_nid_no_patch); + +int APS5_VABI sceJscUnknown_0KClQgT2ckA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0KClQgT2ckA", sceJscUnknown_0KClQgT2ckA_nid_no_patch); + +int APS5_VABI sceJscUnknown_0KqxIyH6DiE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0KqxIyH6DiE", sceJscUnknown_0KqxIyH6DiE_nid_no_patch); + +int APS5_VABI sceJscUnknown_0KsRLd9dTVU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0KsRLd9dTVU", sceJscUnknown_0KsRLd9dTVU_nid_no_patch); + +int APS5_VABI sceJscUnknown_0KynCQXu9XQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0KynCQXu9XQ", sceJscUnknown_0KynCQXu9XQ_nid_no_patch); + +int APS5_VABI sceJscUnknown_0LSRs8rB_A8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0LSRs8rB-A8", sceJscUnknown_0LSRs8rB_A8_nid_no_patch); + +int APS5_VABI sceJscUnknown_0MHM5pZ54E4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0MHM5pZ54E4", sceJscUnknown_0MHM5pZ54E4_nid_no_patch); + +int APS5_VABI sceJscUnknown_0Mo4EsA5wVo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Mo4EsA5wVo", sceJscUnknown_0Mo4EsA5wVo_nid_no_patch); + +int APS5_VABI sceJscUnknown_0MwjtlWeGGg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0MwjtlWeGGg", sceJscUnknown_0MwjtlWeGGg_nid_no_patch); + +int APS5_VABI sceJscUnknown_0OI3DXKmR9w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0OI3DXKmR9w", sceJscUnknown_0OI3DXKmR9w_nid_no_patch); + +int APS5_VABI sceJscUnknown_0OLbLK3fZlE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0OLbLK3fZlE", sceJscUnknown_0OLbLK3fZlE_nid_no_patch); + +int APS5_VABI sceJscUnknown_0QVx_6Ffzjc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0QVx-6Ffzjc", sceJscUnknown_0QVx_6Ffzjc_nid_no_patch); + +int APS5_VABI sceJscUnknown_0QbSxsg9i1M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0QbSxsg9i1M", sceJscUnknown_0QbSxsg9i1M_nid_no_patch); + +int APS5_VABI sceJscUnknown_0QjiF9xEFQA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0QjiF9xEFQA", sceJscUnknown_0QjiF9xEFQA_nid_no_patch); + +int APS5_VABI sceJscUnknown_0R0tdvM7fFs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0R0tdvM7fFs", sceJscUnknown_0R0tdvM7fFs_nid_no_patch); + +int APS5_VABI sceJscUnknown_0RqgTKckJBs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0RqgTKckJBs", sceJscUnknown_0RqgTKckJBs_nid_no_patch); + +int APS5_VABI sceJscUnknown_0SoDdOYaQE4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0SoDdOYaQE4", sceJscUnknown_0SoDdOYaQE4_nid_no_patch); + +int APS5_VABI sceJscUnknown_0Uk_3H5HdEo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Uk-3H5HdEo", sceJscUnknown_0Uk_3H5HdEo_nid_no_patch); + +int APS5_VABI sceJscUnknown_0Ukd6tvlVhs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Ukd6tvlVhs", sceJscUnknown_0Ukd6tvlVhs_nid_no_patch); + +int APS5_VABI sceJscUnknown_0W_mECnGicE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0W-mECnGicE", sceJscUnknown_0W_mECnGicE_nid_no_patch); + +int APS5_VABI sceJscUnknown_0XH9Pu6dPdk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0XH9Pu6dPdk", sceJscUnknown_0XH9Pu6dPdk_nid_no_patch); + +int APS5_VABI sceJscUnknown_0Y_ebXGv50I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Y-ebXGv50I", sceJscUnknown_0Y_ebXGv50I_nid_no_patch); + +int APS5_VABI sceJscUnknown_0YfwqGopI4c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0YfwqGopI4c", sceJscUnknown_0YfwqGopI4c_nid_no_patch); + +int APS5_VABI sceJscUnknown_0Za34L_PPDg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Za34L-PPDg", sceJscUnknown_0Za34L_PPDg_nid_no_patch); + +int APS5_VABI sceJscUnknown_0Zix0_Zcx2k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Zix0+Zcx2k", sceJscUnknown_0Zix0_Zcx2k_nid_no_patch); + +int APS5_VABI sceJscUnknown_0bI_VPZXmzg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0bI-VPZXmzg", sceJscUnknown_0bI_VPZXmzg_nid_no_patch); + +int APS5_VABI sceJscUnknown_0bq1LixdHAY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0bq1LixdHAY", sceJscUnknown_0bq1LixdHAY_nid_no_patch); + +int APS5_VABI sceJscUnknown_0c_07mr9xP8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0c-07mr9xP8", sceJscUnknown_0c_07mr9xP8_nid_no_patch); + +int APS5_VABI sceJscUnknown_0cIPqIrPhvg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0cIPqIrPhvg", sceJscUnknown_0cIPqIrPhvg_nid_no_patch); + +int APS5_VABI sceJscUnknown_0dTfcPHH3dg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0dTfcPHH3dg", sceJscUnknown_0dTfcPHH3dg_nid_no_patch); + +int APS5_VABI sceJscUnknown_0earDv9xPN8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0earDv9xPN8", sceJscUnknown_0earDv9xPN8_nid_no_patch); + +int APS5_VABI sceJscUnknown_0f06o3_XgV0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0f06o3-XgV0", sceJscUnknown_0f06o3_XgV0_nid_no_patch); + +int APS5_VABI sceJscUnknown_0g_jtYCdtQw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0g+jtYCdtQw", sceJscUnknown_0g_jtYCdtQw_nid_no_patch); + +int APS5_VABI sceJscUnknown_0g66of16pvY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0g66of16pvY", sceJscUnknown_0g66of16pvY_nid_no_patch); + +int APS5_VABI sceJscUnknown_0gQImk3C0sA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0gQImk3C0sA", sceJscUnknown_0gQImk3C0sA_nid_no_patch); + +int APS5_VABI sceJscUnknown_0ggQR1NjDCY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0ggQR1NjDCY", sceJscUnknown_0ggQR1NjDCY_nid_no_patch); + +int APS5_VABI sceJscUnknown_0ixkCjQDH2I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0ixkCjQDH2I", sceJscUnknown_0ixkCjQDH2I_nid_no_patch); + +int APS5_VABI sceJscUnknown_0lR2YBKJMIE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0lR2YBKJMIE", sceJscUnknown_0lR2YBKJMIE_nid_no_patch); + +int APS5_VABI sceJscUnknown_0nI81nsrl_A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0nI81nsrl+A", sceJscUnknown_0nI81nsrl_A_nid_no_patch); + +int APS5_VABI sceJscUnknown_0pp8T8yX1BU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0pp8T8yX1BU", sceJscUnknown_0pp8T8yX1BU_nid_no_patch); + +int APS5_VABI sceJscUnknown_0q3Pf6miAts_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0q3Pf6miAts", sceJscUnknown_0q3Pf6miAts_nid_no_patch); + +int APS5_VABI sceJscUnknown_0qmAOS00O2g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0qmAOS00O2g", sceJscUnknown_0qmAOS00O2g_nid_no_patch); + +int APS5_VABI sceJscUnknown_0qr5jdWMC3E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0qr5jdWMC3E", sceJscUnknown_0qr5jdWMC3E_nid_no_patch); + +int APS5_VABI sceJscUnknown_0rj2akkxaO0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0rj2akkxaO0", sceJscUnknown_0rj2akkxaO0_nid_no_patch); + +int APS5_VABI sceJscUnknown_0ro1gFmJat8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0ro1gFmJat8", sceJscUnknown_0ro1gFmJat8_nid_no_patch); + +int APS5_VABI sceJscUnknown_0v8_cEQL93A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0v8-cEQL93A", sceJscUnknown_0v8_cEQL93A_nid_no_patch); + +int APS5_VABI sceJscUnknown_0vAcIjIOiEE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0vAcIjIOiEE", sceJscUnknown_0vAcIjIOiEE_nid_no_patch); + +int APS5_VABI sceJscUnknown_0wxHtqo7dew_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0wxHtqo7dew", sceJscUnknown_0wxHtqo7dew_nid_no_patch); + +int APS5_VABI sceJscUnknown_0wzoLEfYeJo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0wzoLEfYeJo", sceJscUnknown_0wzoLEfYeJo_nid_no_patch); + +int APS5_VABI sceJscUnknown_0xH4JecjF2Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0xH4JecjF2Q", sceJscUnknown_0xH4JecjF2Q_nid_no_patch); } diff --git a/docs/dev/debt/libSceJsc.md b/docs/dev/debt/libSceJsc.md new file mode 100644 index 0000000000..1d0a1f71e9 --- /dev/null +++ b/docs/dev/debt/libSceJsc.md @@ -0,0 +1,5 @@ +# libSceJsc technical debt + +### Unknown function info + +- The 150 `sceJscUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceJsc/Export.cpp) are present in the PS5 3.20 `libSceJsc` export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their NIDs are derived from exported mangled symbols. Prototypes and behavior are unverified; each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. These declarations make no ABI or behavior claim. From 39e19da487f7bf5dcf68225cfddc023f80ec1633 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:15:36 +0100 Subject: [PATCH 042/702] docs: distinguish known exports from unverified ABI --- docs/dev/debt/libSceJsc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceJsc.md b/docs/dev/debt/libSceJsc.md index 1d0a1f71e9..4df4f58370 100644 --- a/docs/dev/debt/libSceJsc.md +++ b/docs/dev/debt/libSceJsc.md @@ -1,5 +1,5 @@ # libSceJsc technical debt -### Unknown function info +### Known export names; ABI prototypes unverified - The 150 `sceJscUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceJsc/Export.cpp) are present in the PS5 3.20 `libSceJsc` export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their NIDs are derived from exported mangled symbols. Prototypes and behavior are unverified; each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. These declarations make no ABI or behavior claim. From 71b3da35b8996515addc22e5ac4a7d2a1dc74b23 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:01:01 +0100 Subject: [PATCH 043/702] fix(libSceFreeTypeOl): name known FreeType aliases --- core/libs/prx/libSceFreeTypeOl/Export.cpp | 84 +++++++++++------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOl/Export.cpp b/core/libs/prx/libSceFreeTypeOl/Export.cpp index 99d7406eca..5a7e47a493 100644 --- a/core/libs/prx/libSceFreeTypeOl/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOl/Export.cpp @@ -440,10 +440,10 @@ int APS5_VABI sceFreeTypeOlUnknown_Uw_BZJpflxk_nid_no_patch() { } APS5_EXPORT("Uw+BZJpflxk", sceFreeTypeOlUnknown_Uw_BZJpflxk_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_D1l8_Bu__Y8_nid_no_patch() { +int APS5_VABI FT_Angle_Diff_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("D1l8+Bu+-Y8", sceFreeTypeOlUnknown_D1l8_Bu__Y8_nid_no_patch); +APS5_EXPORT("D1l8+Bu+-Y8", FT_Angle_Diff_nid_no_patch); int APS5_VABI sceFreeTypeOlUnknown_p90G4PB_rPc_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -460,30 +460,30 @@ int APS5_VABI sceFreeTypeOlUnknown_ZqvTX8u1bEc_nid_no_patch() { } APS5_EXPORT("ZqvTX8u1bEc", sceFreeTypeOlUnknown_ZqvTX8u1bEc_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_cR_yQldyXvs_nid_no_patch() { +int APS5_VABI FT_Bitmap_Convert_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("cR-yQldyXvs", sceFreeTypeOlUnknown_cR_yQldyXvs_nid_no_patch); +APS5_EXPORT("cR-yQldyXvs", FT_Bitmap_Convert_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_1GL9pCFuiHQ_nid_no_patch() { +int APS5_VABI FT_Bitmap_Copy_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("1GL9pCFuiHQ", sceFreeTypeOlUnknown_1GL9pCFuiHQ_nid_no_patch); +APS5_EXPORT("1GL9pCFuiHQ", FT_Bitmap_Copy_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_Mu2upxmY2C8_nid_no_patch() { +int APS5_VABI FT_Bitmap_Done_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Mu2upxmY2C8", sceFreeTypeOlUnknown_Mu2upxmY2C8_nid_no_patch); +APS5_EXPORT("Mu2upxmY2C8", FT_Bitmap_Done_nid_no_patch); int APS5_VABI sceFreeTypeOlUnknown_wcMdf_kMx7o_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("wcMdf-kMx7o", sceFreeTypeOlUnknown_wcMdf_kMx7o_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown__AeE58GkM_Q_nid_no_patch() { +int APS5_VABI FT_Bitmap_New_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("-AeE58GkM+Q", sceFreeTypeOlUnknown__AeE58GkM_Q_nid_no_patch); +APS5_EXPORT("-AeE58GkM+Q", FT_Bitmap_New_nid_no_patch); int APS5_VABI sceFreeTypeOlUnknown_h0n4Fb9j2R8_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -510,10 +510,10 @@ int APS5_VABI sceFreeTypeOlUnknown_pO0sA7m_UKY_nid_no_patch() { } APS5_EXPORT("pO0sA7m+UKY", sceFreeTypeOlUnknown_pO0sA7m_UKY_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_yD4_QaNSink_nid_no_patch() { +int APS5_VABI FT_Cos_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("yD4-QaNSink", sceFreeTypeOlUnknown_yD4_QaNSink_nid_no_patch); +APS5_EXPORT("yD4-QaNSink", FT_Cos_nid_no_patch); int APS5_VABI sceFreeTypeOlUnknown_fp_jO_c8Rfk_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -600,60 +600,60 @@ int APS5_VABI sceFreeTypeOlUnknown_l55oKq6zmrA_nid_no_patch() { } APS5_EXPORT("l55oKq6zmrA", sceFreeTypeOlUnknown_l55oKq6zmrA_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_NLwiYXdX5WU_nid_no_patch() { +int APS5_VABI FT_Get_CID_From_Glyph_Index_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("NLwiYXdX5WU", sceFreeTypeOlUnknown_NLwiYXdX5WU_nid_no_patch); +APS5_EXPORT("NLwiYXdX5WU", FT_Get_CID_From_Glyph_Index_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_p_LFZYbDPDU_nid_no_patch() { +int APS5_VABI FT_Get_CID_Is_Internally_CID_Keyed_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("p-LFZYbDPDU", sceFreeTypeOlUnknown_p_LFZYbDPDU_nid_no_patch); +APS5_EXPORT("p-LFZYbDPDU", FT_Get_CID_Is_Internally_CID_Keyed_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_Pr3eI2uGgvU_nid_no_patch() { +int APS5_VABI FT_Get_CID_Registry_Ordering_Supplement_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Pr3eI2uGgvU", sceFreeTypeOlUnknown_Pr3eI2uGgvU_nid_no_patch); +APS5_EXPORT("Pr3eI2uGgvU", FT_Get_CID_Registry_Ordering_Supplement_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_MXgrzq0nh1Q_nid_no_patch() { +int APS5_VABI FT_Get_CMap_Format_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("MXgrzq0nh1Q", sceFreeTypeOlUnknown_MXgrzq0nh1Q_nid_no_patch); +APS5_EXPORT("MXgrzq0nh1Q", FT_Get_CMap_Format_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_2UZg_FAym4c_nid_no_patch() { +int APS5_VABI FT_Get_CMap_Language_ID_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("2UZg+FAym4c", sceFreeTypeOlUnknown_2UZg_FAym4c_nid_no_patch); +APS5_EXPORT("2UZg+FAym4c", FT_Get_CMap_Language_ID_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_edmDMsPE7Zg_nid_no_patch() { +int APS5_VABI FT_Get_Charmap_Index_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("edmDMsPE7Zg", sceFreeTypeOlUnknown_edmDMsPE7Zg_nid_no_patch); +APS5_EXPORT("edmDMsPE7Zg", FT_Get_Charmap_Index_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_H4eyE4ez7O4_nid_no_patch() { +int APS5_VABI FT_Get_FSType_Flags_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("H4eyE4ez7O4", sceFreeTypeOlUnknown_H4eyE4ez7O4_nid_no_patch); +APS5_EXPORT("H4eyE4ez7O4", FT_Get_FSType_Flags_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_l4azYkX86Ds_nid_no_patch() { +int APS5_VABI FT_Get_Gasp_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("l4azYkX86Ds", sceFreeTypeOlUnknown_l4azYkX86Ds_nid_no_patch); +APS5_EXPORT("l4azYkX86Ds", FT_Get_Gasp_nid_no_patch); int APS5_VABI sceFreeTypeOlUnknown_eMKHL_Cj50k_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("eMKHL+Cj50k", sceFreeTypeOlUnknown_eMKHL_Cj50k_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_tGmYdLIYT98_nid_no_patch() { +int APS5_VABI FT_Get_Glyph_Name_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("tGmYdLIYT98", sceFreeTypeOlUnknown_tGmYdLIYT98_nid_no_patch); +APS5_EXPORT("tGmYdLIYT98", FT_Get_Glyph_Name_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_LPtlB241JSg_nid_no_patch() { +int APS5_VABI FT_Get_Kerning_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("LPtlB241JSg", sceFreeTypeOlUnknown_LPtlB241JSg_nid_no_patch); +APS5_EXPORT("LPtlB241JSg", FT_Get_Kerning_nid_no_patch); int APS5_VABI sceFreeTypeOlUnknown_qJFh32pZAJQ_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -675,10 +675,10 @@ int APS5_VABI sceFreeTypeOlUnknown_pORpGtdlfqQ_nid_no_patch() { } APS5_EXPORT("pORpGtdlfqQ", sceFreeTypeOlUnknown_pORpGtdlfqQ_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_sZzliEQrICM_nid_no_patch() { +int APS5_VABI FT_Get_Name_Index_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("sZzliEQrICM", sceFreeTypeOlUnknown_sZzliEQrICM_nid_no_patch); +APS5_EXPORT("sZzliEQrICM", FT_Get_Name_Index_nid_no_patch); int APS5_VABI sceFreeTypeOlUnknown_NVuvsPgTXto_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -710,25 +710,25 @@ int APS5_VABI sceFreeTypeOlUnknown_Ik17GQynFpw_nid_no_patch() { } APS5_EXPORT("Ik17GQynFpw", sceFreeTypeOlUnknown_Ik17GQynFpw_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_GVnNjpJWCXM_nid_no_patch() { +int APS5_VABI FT_Get_Postscript_Name_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("GVnNjpJWCXM", sceFreeTypeOlUnknown_GVnNjpJWCXM_nid_no_patch); +APS5_EXPORT("GVnNjpJWCXM", FT_Get_Postscript_Name_nid_no_patch); int APS5_VABI sceFreeTypeOlUnknown_1wFNGIk3FR0_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("1wFNGIk3FR0", sceFreeTypeOlUnknown_1wFNGIk3FR0_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_B4_zPp0EATE_nid_no_patch() { +int APS5_VABI FT_Get_Sfnt_Name_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("B4-zPp0EATE", sceFreeTypeOlUnknown_B4_zPp0EATE_nid_no_patch); +APS5_EXPORT("B4-zPp0EATE", FT_Get_Sfnt_Name_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_fbRv4M_XzL8_nid_no_patch() { +int APS5_VABI FT_Get_Sfnt_Name_Count_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("fbRv4M+XzL8", sceFreeTypeOlUnknown_fbRv4M_XzL8_nid_no_patch); +APS5_EXPORT("fbRv4M+XzL8", FT_Get_Sfnt_Name_Count_nid_no_patch); int APS5_VABI sceFreeTypeOlUnknown_omDz0x8lyHk_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -750,9 +750,9 @@ int APS5_VABI sceFreeTypeOlUnknown_eSFTUAm1wf8_nid_no_patch() { } APS5_EXPORT("eSFTUAm1wf8", sceFreeTypeOlUnknown_eSFTUAm1wf8_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_aWMUKhiwUbk_nid_no_patch() { +int APS5_VABI FT_Get_X11_Font_Format_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("aWMUKhiwUbk", sceFreeTypeOlUnknown_aWMUKhiwUbk_nid_no_patch); +APS5_EXPORT("aWMUKhiwUbk", FT_Get_X11_Font_Format_nid_no_patch); } From ad9a0c8ef905519fc1eaa56ac34afc5e974a2b4c Mon Sep 17 00:00:00 2001 From: frankea Date: Sat, 24 Oct 2026 03:34:54 -0400 Subject: [PATCH 044/702] fix(libkernel): report 12 GiB of direct memory, as a PS5 does sceKernelGetDirectMemorySize returns 0x300000000 inside Gran Turismo 7 (PPSA01317) on a PS5 with firmware 13.40. DIRECT_MEMORY_SIZE was 13824 MiB. --- core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp | 2 +- core/libs/tests/GuestAprRead.cpp | 2 +- core/libs/tests/GuestMemory.cpp | 5 +++++ docs/dev/debt/libkernel.md | 2 +- docs/user/USAGE.md | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 604e85b34c..a04a9c3481 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -7,7 +7,7 @@ #include "prx/libkernel/KernelErrors.hpp" -static constexpr size_t DIRECT_MEMORY_SIZE = 13824ULL * 1024 * 1024; +static constexpr size_t DIRECT_MEMORY_SIZE = 12288ULL * 1024 * 1024; static constexpr size_t PS5_PAGE_SIZE = 0x4000; #ifdef _WIN32 diff --git a/core/libs/tests/GuestAprRead.cpp b/core/libs/tests/GuestAprRead.cpp index 004346dbcd..bfe04340ce 100644 --- a/core/libs/tests/GuestAprRead.cpp +++ b/core/libs/tests/GuestAprRead.cpp @@ -68,7 +68,7 @@ private: class DirectBuffer { public: DirectBuffer() { - Require(sceKernelAllocateDirectMemory(0, 13824ll << 20, MappingBytes, 0x4000, 0, &physical) == 0, "cannot allocate direct memory"); + Require(sceKernelAllocateDirectMemory(0, 12288ll << 20, MappingBytes, 0x4000, 0, &physical) == 0, "cannot allocate direct memory"); if (sceKernelMapDirectMemory(&pointer, MappingBytes, 0x33, 0, physical, 0x4000) != 0) { sceKernelReleaseDirectMemory(physical, MappingBytes); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 7f78313a05..c9418080fe 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -705,6 +705,10 @@ static void CheckReleaseDirectMemoryClearsMappings() { } } +static void CheckDirectMemorySize() { + Require(sceKernelGetDirectMemorySize() == 0x300000000ull); +} + static void CheckReleaseDirectMemoryRejectsInvalidRanges() { constexpr std::size_t page = 0x4000; Require(sceKernelReleaseDirectMemory(-1, page) == SCE_KERNEL_ERROR_EINVAL); @@ -3348,6 +3352,7 @@ int main(int argc, char** argv) { CheckSavedDirectMemorySurvivesCachePressure(); CheckReusedDirectMemorySections(); CheckReleaseDirectMemoryClearsMappings(); + CheckDirectMemorySize(); CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); CheckFixedMappingOverHoleInsideAllocation(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index cc17825081..eab1c99bfb 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -54,7 +54,7 @@ - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. The host pages behind a released range stay committed until `sceKernelMunmap`, as `madvise` keeps them for every advice; freeing them would lose the contents the console still returns. -- [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) +- [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 12 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - its arguments follow a payload process on a PS5 (firmware 13.60) ([probes](https://gist.github.com/orezraey/42a06a7ea0d9fb57ac9c06eaf9e6756c)). One answer is not modelled: in a process that had not yet called `sceKernelGetDirectMemorySize` or otherwise touched direct memory, the console returned `EBADF` (`0x80020009`); here the call always searches the pool. Titles open the direct-memory device during start-up, so they are not expected to see it - [sceKernelReleaseDirectMemory](../../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - releases the allocated pages of the range and skips pages that are not allocated, returning 0, while `sceKernelCheckedReleaseDirectMemory` returns `ENOENT` for them, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c206f35a546230d9c9808b823c0f36f5850cd4af/src/core/memory.cpp#L295-L313) does; not measured on a console. Absolum (PPSA28311) releases such a range when it disposes of its video logos - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 6c27fdd74f..02bb9a5576 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -205,7 +205,7 @@ Use the generated files printed as `Guest module:` for bundled title modules. `l On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. With `APS5_MIMALLOC` set, the title loads `mimalloc.dll` and `mimalloc-redirect.dll` from `libs/` and allocates through mimalloc. Copy both from `build/core/libs/libs`; the release archives contain them. Without them the title keeps the C runtime's heap and prints nothing. -On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. +On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 12288 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. A range larger than 64 MiB is imported as one sparse buffer over 64 MiB udmabuf buffers when the device binds sparse memory (RADV, ANV). Without sparse binding, or when the device's sparse block size is larger than 64 KiB, ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. From bc8562f74640648e9d3726f64b13f1e11bebe12f Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 01:56:43 -0300 Subject: [PATCH 045/702] fix(libkernel): preserve AIO request ownership and identities --- core/libs/prx/libkernel/Aio/src/Aio.cpp | 112 ++++++++---- core/libs/prx/libkernel/CMakeLists.txt | 8 + core/libs/prx/libkernel/include/Api.hpp | 1 + core/libs/tests/GuestAio.cpp | 215 ++++++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 5 files changed, 307 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index 54fb54921f..7d4c597a60 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -24,7 +25,6 @@ namespace { constexpr std::int32_t AioProcessing = 2; constexpr std::int32_t AioCompleted = 3; -constexpr std::int32_t AioAborted = 4; constexpr std::int32_t AioReported = 0x10000; constexpr std::int32_t AioMaxQueues = 512; constexpr std::int32_t AioRequestNumMax = 128; @@ -42,51 +42,94 @@ constexpr std::int32_t AioDefaultDelayedCountLimit = 32; constexpr std::uint32_t AioDefaultSplitSize = 0x100000; std::mutex g_mutex; -std::array g_states{}; -std::int32_t g_nextId = 1; +struct RequestState { + std::int32_t id = 0; + std::int32_t state = 0; +}; +std::array g_requests{}; +std::int64_t g_nextId = 1; -bool ValidId(std::int32_t id) { - return id > 0 && id < AioMaxQueues; +RequestState* FindLocked(std::int32_t id) { + if (id <= 0) return nullptr; + for (auto& request : g_requests) { + if (request.id == id) return &request; + } + return nullptr; } -std::int32_t AllocateId() { - std::lock_guard lock(g_mutex); - const std::int32_t id = g_nextId; - g_nextId = (g_nextId + 1) % AioMaxQueues; - if (g_nextId == 0) g_nextId = 1; - g_states[id] = AioProcessing; - return id; -} +class Reservation { +public: + explicit Reservation(std::int32_t count) : count(count) { + std::lock_guard lock(g_mutex); + std::array slots{}; + std::int32_t available = 0; + for (auto& request : g_requests) { + if (request.id == 0) slots[available++] = &request; + if (available == count) break; + } + if (available != count) throw std::runtime_error("AIO: host request registry exhausted"); + if (g_nextId + count - 1 > std::numeric_limits::max()) + throw std::runtime_error("AIO: host request identities exhausted"); + for (std::int32_t index = 0; index < count; ++index) { + ids[index] = static_cast(g_nextId++); + *slots[index] = {ids[index], AioProcessing}; + } + } -void SetState(std::int32_t id, std::int32_t state) { - std::lock_guard lock(g_mutex); - g_states[id] = state; -} + ~Reservation() { + if (published) return; + std::lock_guard lock(g_mutex); + for (std::int32_t index = 0; index < count; ++index) { + if (auto* request = FindLocked(ids[index])) *request = {}; + } + } + + Reservation(const Reservation&) = delete; + Reservation& operator=(const Reservation&) = delete; + + void Publish(std::int32_t* output) { + std::lock_guard lock(g_mutex); + for (std::int32_t index = 0; index < count; ++index) { + output[index] = ids[index]; + FindLocked(ids[index])->state = AioCompleted; + } + published = true; + } + +private: + std::array ids{}; + std::int32_t count; + bool published = false; +}; std::int32_t StateLocked(std::int32_t id) { - return ValidId(id) && g_states[id] != 0 ? g_states[id] : SCE_KERNEL_ERROR_ESRCH; + const auto* request = FindLocked(id); + return request != nullptr ? request->state : SCE_KERNEL_ERROR_ESRCH; } std::int32_t ReportLocked(std::int32_t id) { const std::int32_t state = StateLocked(id); - if (state != SCE_KERNEL_ERROR_ESRCH && state != AioProcessing) g_states[id] |= AioReported; + if (state != SCE_KERNEL_ERROR_ESRCH && state != AioProcessing) FindLocked(id)->state |= AioReported; return state; } std::int32_t CancelLocked(std::int32_t id) { const std::int32_t state = StateLocked(id); if (state == SCE_KERNEL_ERROR_ESRCH) return state; - if (state != AioProcessing) return state & ~AioReported; - g_states[id] = AioAborted; - return AioAborted; + return state & ~AioReported; } std::int32_t DeleteLocked(std::int32_t id) { - if (StateLocked(id) == SCE_KERNEL_ERROR_ESRCH) return SCE_KERNEL_ERROR_ESRCH; - g_states[id] = 0; + auto* request = FindLocked(id); + if (request == nullptr) return SCE_KERNEL_ERROR_ESRCH; + *request = {}; return 0; } +void RequireFinishedLocked(std::int32_t id, const char* function) { + if (StateLocked(id) == AioProcessing) NotImplemented_nid_no_patch(function); +} + std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int64_t offset) { const GuestArena::HostWrite destination(buf, nbyte); if (!destination.Open()) { @@ -152,22 +195,20 @@ int ValidateRequests(KernelAioRwRequest* req, std::int32_t size, std::int32_t pr int SubmitCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, bool write, std::int32_t* id) { const int error = ValidateRequests(req, size, prio, id); if (error != 0) return error; - const std::int32_t queue = AllocateId(); + Reservation reservation(1); for (std::int32_t i = 0; i < size; ++i) RunRequest(req[i], write); - SetState(queue, AioCompleted); - *id = queue; + reservation.Publish(id); return 0; } int SubmitCommandsMultiple(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, bool write, std::int32_t* id) { const int error = ValidateRequests(req, size, prio, id); if (error != 0) return error; + Reservation reservation(size); for (std::int32_t i = 0; i < size; ++i) { - const std::int32_t queue = AllocateId(); RunRequest(req[i], write); - SetState(queue, AioCompleted); - id[i] = queue; } + reservation.Publish(id); return 0; } @@ -181,9 +222,17 @@ int ValidateIds(const std::int32_t* id, std::int32_t num, const void* out) { extern "C" { +void AioAdvanceIdentityForTest_nid_no_patch(std::int64_t next) { + std::lock_guard lock(g_mutex); + if (next < g_nextId || next > static_cast(std::numeric_limits::max()) + 1) + throw std::invalid_argument("AIO: invalid test identity advance"); + g_nextId = next; +} + int APS5_VABI sceKernelAioDeleteRequest(int32_t id, int32_t* ret) { if (ret == nullptr) return SCE_KERNEL_ERROR_EFAULT; std::lock_guard lock(g_mutex); + RequireFinishedLocked(id, __func__); *ret = DeleteLocked(id); return 0; } @@ -192,6 +241,7 @@ int APS5_VABI sceKernelAioDeleteRequests(int32_t* id, int32_t num, int32_t* ret) const int error = ValidateIds(id, num, ret); if (error != 0) return error; std::lock_guard lock(g_mutex); + for (int32_t i = 0; i < num; ++i) RequireFinishedLocked(id[i], __func__); for (int32_t i = 0; i < num; ++i) ret[i] = DeleteLocked(id[i]); return 0; } @@ -199,6 +249,7 @@ int APS5_VABI sceKernelAioDeleteRequests(int32_t* id, int32_t num, int32_t* ret) int APS5_VABI sceKernelAioCancelRequest(int32_t id, int32_t* state) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; std::lock_guard lock(g_mutex); + RequireFinishedLocked(id, __func__); *state = CancelLocked(id); return 0; } @@ -207,6 +258,7 @@ int APS5_VABI sceKernelAioCancelRequests(int32_t* id, int32_t num, int32_t* stat const int error = ValidateIds(id, num, state); if (error != 0) return error; std::lock_guard lock(g_mutex); + for (int32_t i = 0; i < num; ++i) RequireFinishedLocked(id[i], __func__); for (int32_t i = 0; i < num; ++i) state[i] = CancelLocked(id[i]); return 0; } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9da2728d50..49d66fbb78 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -487,6 +487,14 @@ if(BUILD_TESTING) target_link_libraries(guest_aio_tests PRIVATE libkernel libc) configure_windows_unwind(guest_aio_tests) add_test(NAME guest_aio COMMAND guest_aio_tests) + foreach(mode identity admission rollback identity-exhaustion processing processing-rollback) + add_test(NAME guest_aio_${mode} COMMAND guest_aio_tests ${mode}) + set_tests_properties(guest_aio_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_aio_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() 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 diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index 35b536672a..59e45eacce 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -186,6 +186,7 @@ int APS5_VABI sceKernelAioPollRequest(int32_t id, int32_t* state); int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, uint32_t mode, uint32_t* usec); int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec); int APS5_VABI sceKernelAioPollRequests(int32_t* id, int32_t num, int32_t* state); +void AioAdvanceIdentityForTest_nid_no_patch(std::int64_t next); int APS5_VABI sceKernelAprResolveFilepathsToIds(const char** paths, uint32_t count, uint32_t* ids, uint32_t* errorIndex); int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* errorIndex); int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* errorIndex); diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 79dfd69a61..247a61420c 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -4,15 +4,19 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libc/include/GuestArena.hpp" #include #include #include +#include #include #include #include #include #include #include +#include +#include #include #include #include @@ -29,6 +33,210 @@ static void Require(bool value, int line) { } } #define Check(value) Require((value), __LINE__) + +static void DeleteAio(std::int32_t id) { + std::int32_t removed = -1; + Check(sceKernelAioDeleteRequest(id, &removed) == 0 && removed == 0); +} + +static std::int32_t SubmitEmptyAio() { + KernelAioResult result{-1, 0}; + KernelAioRwRequest request{0, 0, nullptr, &result, -1}; + std::int32_t id = 0; + Check(sceKernelAioSubmitReadCommands(&request, 1, 1, &id) == 0); + Check(result.state == 3 && result.return_value == SCE_KERNEL_ERROR_EBADF); + return id; +} + +static void CheckAioIdentity() { + const auto retained = SubmitEmptyAio(); + std::int32_t state = 0; + Check(sceKernelAioPollRequest(retained, &state) == 0 && state == 3); + auto previous = retained; + for (int iteration = 0; iteration < 1100; ++iteration) { + const auto id = SubmitEmptyAio(); + Check(id > previous); + previous = id; + DeleteAio(id); + Check(sceKernelAioPollRequest(retained, &state) == 0 && state == 0x10003); + } + DeleteAio(retained); + for (int iteration = 0; iteration < 1100; ++iteration) { + const auto id = SubmitEmptyAio(); + Check(id > previous); + previous = id; + DeleteAio(id); + } + Check(sceKernelAioPollRequest(retained, &state) == 0 && state == SCE_KERNEL_ERROR_ESRCH); +} + +static void CheckAioAdmission() { + const auto file = std::filesystem::path("anyps5-aio-admission-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + { std::ofstream stream(file, std::ios::binary); stream << 'Z'; Check(static_cast(stream)); } + const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Check(fd >= 0); + std::vector retained; + for (int index = 0; index < 510; ++index) retained.push_back(SubmitEmptyAio()); + std::array results{{{-7, 7}, {-7, 7}}}; + char byte = 'X'; + KernelAioRwRequest requests[2]{{0, 1, &byte, &results[0], fd}, {0, 1, &byte, &results[1], fd}}; + std::int32_t state = 0; + Check(sceKernelAioPollRequest(retained[0], &state) == 0 && state == 3); + for (const auto submit : {sceKernelAioSubmitReadCommandsMultiple, sceKernelAioSubmitWriteCommandsMultiple}) { + std::int32_t ids[2]{-7, -7}; + bool refused = false; + try { submit(requests, 2, 1, ids); } catch (const std::runtime_error&) { refused = true; } + Check(refused && ids[0] == -7 && ids[1] == -7); + Check(results[0].state == 7 && results[0].return_value == -7 && results[1].state == 7 && results[1].return_value == -7); + } + retained.push_back(SubmitEmptyAio()); + for (const auto submit : {sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands}) { + std::int32_t id = -7; + bool refused = false; + try { submit(requests, 1, 1, &id); } catch (const std::runtime_error&) { refused = true; } + Check(refused && id == -7 && results[0].state == 7 && results[0].return_value == -7); + } + Check(sceKernelAioPollRequest(retained[0], &state) == 0 && state == 0x10003); + { std::ifstream stream(file, std::ios::binary); Check(stream.get() == 'Z' && stream.get() == std::char_traits::eof()); } + for (const auto id : retained) DeleteAio(id); + std::int32_t ids[2]{}; + Check(sceKernelAioSubmitReadCommandsMultiple(requests, 2, 1, ids) == 0); + Check(ids[0] != ids[1] && results[0].state == 3 && results[1].state == 3); + for (const auto id : ids) DeleteAio(id); + Check(sceKernelClose(fd) == 0 && std::filesystem::remove(file)); +} + +static int g_aioSourceCalls = 0; + +static void FailAioSource(std::uintptr_t, std::size_t) { + if (++g_aioSourceCalls % 2 == 0) throw std::runtime_error("AIO source fault injection"); +} + +static void CheckAioRollback() { + const auto file = std::filesystem::path("anyps5-aio-rollback-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + { std::ofstream stream(file, std::ios::binary); stream << "zz"; Check(static_cast(stream)); } + const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Check(fd >= 0); + std::array results{{{-7, 7}, {-7, 7}}}; + char byte = 'A'; + KernelAioRwRequest requests[2]{{0, 1, &byte, &results[0], fd}, {1, 1, &byte, &results[1], fd}}; + for (const auto submit : {sceKernelAioSubmitWriteCommands, sceKernelAioSubmitWriteCommandsMultiple}) { + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(FailAioSource); + for (int iteration = 0; iteration < 600; ++iteration) { + results = {{{-7, 7}, {-7, 7}}}; + std::int32_t ids[2]{-7, -7}; + bool refused = false; + const auto calls = g_aioSourceCalls; + try { submit(requests, 2, 1, ids); } catch (const std::runtime_error& error) { refused = std::strcmp(error.what(), "AIO source fault injection") == 0; } + Check(refused && ids[0] == -7 && ids[1] == -7); + Check(g_aioSourceCalls == calls + 2 && results[0].state == 3 && results[0].return_value == 1 && results[1].state == 7); + } + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(nullptr); + } + Check(sceKernelClose(fd) == 0); + { std::ifstream stream(file, std::ios::binary); Check(stream.get() == 'A' && stream.get() == 'z' && stream.get() == std::char_traits::eof()); } + Check(std::filesystem::remove(file)); + CheckAioAdmission(); +} + +static void CheckAioIdentityExhaustion() { + const auto maximum = std::numeric_limits::max(); + AioAdvanceIdentityForTest_nid_no_patch(maximum); + std::array results{{{-7, 7}, {-7, 7}}}; + KernelAioRwRequest requests[2]{{0, 0, nullptr, &results[0], -1}, {0, 0, nullptr, &results[1], -1}}; + std::int32_t ids[2]{-7, -7}; + bool refused = false; + try { sceKernelAioSubmitReadCommandsMultiple(requests, 2, 1, ids); } catch (const std::runtime_error&) { refused = true; } + Check(refused && ids[0] == -7 && ids[1] == -7 && results[0].state == 7 && results[1].state == 7); + const auto final = SubmitEmptyAio(); + Check(final == maximum); + DeleteAio(final); + refused = false; + try { sceKernelAioSubmitReadCommands(requests, 1, 1, ids); } catch (const std::runtime_error&) { refused = true; } + Check(refused && ids[0] == -7 && results[0].state == 7); + std::int32_t state = 0; + Check(sceKernelAioPollRequest(final, &state) == 0 && state == SCE_KERNEL_ERROR_ESRCH); +} + +static std::mutex g_aioGateMutex; +static std::condition_variable g_aioGateChanged; +static bool g_aioGateEntered = false; +static bool g_aioGateReleased = false; +static bool g_aioGateFails = false; + +static void HoldAioSource(std::uintptr_t, std::size_t) { + std::unique_lock lock(g_aioGateMutex); + g_aioGateEntered = true; + g_aioGateChanged.notify_all(); + Check(g_aioGateChanged.wait_for(lock, std::chrono::seconds(5), [] { return g_aioGateReleased; })); + if (g_aioGateFails) throw std::runtime_error("AIO gated transfer failure"); +} + +static void CheckAioProcessingOwnership(bool fail) { + g_aioGateFails = fail; + const auto maximum = std::numeric_limits::max(); + AioAdvanceIdentityForTest_nid_no_patch(maximum - 2); + const auto completed = SubmitEmptyAio(); + const auto pending = completed + 1; + KernelAioResult result{-7, 7}; + char byte = 'x'; + KernelAioRwRequest request{0, 1, &byte, &result, -1}; + std::int32_t submittedId = -7; + bool submitFailed = false; + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(HoldAioSource); + std::thread submitter([&] { + try { Check(sceKernelAioSubmitWriteCommands(&request, 1, 1, &submittedId) == 0); } + catch (const std::runtime_error& error) { submitFailed = std::strcmp(error.what(), "AIO gated transfer failure") == 0; } + }); + { + std::unique_lock lock(g_aioGateMutex); + Check(g_aioGateChanged.wait_for(lock, std::chrono::seconds(5), [] { return g_aioGateEntered; })); + } + std::int32_t state = -7; + Check(sceKernelAioPollRequest(pending, &state) == 0 && state == 2); + for (const auto operation : {sceKernelAioCancelRequest, sceKernelAioDeleteRequest}) { + state = -7; + bool refused = false; + try { operation(pending, &state); } catch (const std::runtime_error&) { refused = true; } + Check(refused && state == -7); + } + for (const auto operation : {sceKernelAioCancelRequests, sceKernelAioDeleteRequests}) { + std::int32_t ids[2]{completed, pending}; + std::int32_t states[2]{-7, -7}; + bool refused = false; + try { operation(ids, 2, states); } catch (const std::runtime_error&) { refused = true; } + Check(refused && states[0] == -7 && states[1] == -7); + } + Check(sceKernelAioPollRequest(completed, &state) == 0 && state == 3); + Check(sceKernelAioPollRequest(pending, &state) == 0 && state == 2); + std::uint32_t timeout = 1000; + Check(sceKernelAioWaitRequest(pending, &state, &timeout) == SCE_KERNEL_ERROR_ETIMEDOUT && state == 2); + std::atomic waiterDone{false}; + std::int32_t waited = -7; + std::thread waiter([&] { + Check(sceKernelAioWaitRequest(pending, &waited, nullptr) == 0); + waiterDone.store(true); + }); + const auto concurrent = SubmitEmptyAio(); + Check(!waiterDone.load() && result.state == 7 && submittedId == -7); + { + std::lock_guard lock(g_aioGateMutex); + g_aioGateReleased = true; + g_aioGateChanged.notify_all(); + } + submitter.join(); + waiter.join(); + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(nullptr); + Check(waiterDone.load() && submitFailed == fail); + if (fail) { + Check(waited == SCE_KERNEL_ERROR_ESRCH && submittedId == -7 && result.state == 7); + } else { + Check(waited == 3 && submittedId == pending); + Check(result.state == 3 && result.return_value == SCE_KERNEL_ERROR_EBADF); + DeleteAio(pending); + } + for (const auto id : {completed, concurrent}) DeleteAio(id); +} #ifdef _WIN32 static int g_parameterReports = 0; static void CountParameterReport(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) { @@ -67,6 +275,12 @@ static void CheckBadPositionedDescriptors() { } #endif int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "identity") == 0) { CheckAioIdentity(); return 0; } + if (argc > 1 && std::strcmp(argv[1], "admission") == 0) { CheckAioAdmission(); return 0; } + if (argc > 1 && std::strcmp(argv[1], "rollback") == 0) { CheckAioRollback(); return 0; } + if (argc > 1 && std::strcmp(argv[1], "identity-exhaustion") == 0) { CheckAioIdentityExhaustion(); return 0; } + if (argc > 1 && std::strcmp(argv[1], "processing") == 0) { CheckAioProcessingOwnership(false); return 0; } + if (argc > 1 && std::strcmp(argv[1], "processing-rollback") == 0) { CheckAioProcessingOwnership(true); return 0; } #ifdef _WIN32 if (argc > 1 && std::strcmp(argv[1], "bad-descriptors") == 0) { CheckBadPositionedDescriptors(); @@ -413,6 +627,7 @@ int main(int argc, char** argv) { result.return_value != 16 || std::memcmp(bytes.data(), sharedContents.data() + offset, 16) != 0) { misread = true; } + DeleteAio(requestId); } }); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index fd41607f44..7061391bd4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -167,6 +167,7 @@ - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; 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; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - the limits and `sceKernelAioInitializeParam`'s defaults (three 20-byte scheduling structs, the last without split) are measured on a PS5 (firmware 13.60), where a null struct crashes both calls; here both throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit +- [AIO request ownership](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) - pending cancel and delete throw: their console behavior and native cancellation guarantees are unverified. The host registry holds at most 511 undeleted submissions, reserves a Multiple batch together and uses positive identities without reuse; capacity or identity exhaustion throws instead of returning an unverified console error. A thrown transfer releases its unpublished reservations without undoing earlier transfers or result stores. Console capacity, identity allocation and exhaustion behavior are unmeasured. Background execution requires atomic retention of the native file and archive/shared-memory metadata across descriptor close and replacement; the existing duplication path does not provide that boundary - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store - [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified From 4c3ac82dea57f70bfd9a14682f017216e3a388ed Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 02:03:36 -0300 Subject: [PATCH 046/702] test(libkernel): observe processing waiters before releasing AIO --- core/libs/prx/libkernel/Aio/src/Aio.cpp | 26 +++++++++++++++++++++++++ core/libs/prx/libkernel/include/Api.hpp | 1 + core/libs/tests/GuestAio.cpp | 4 ++++ 3 files changed, 31 insertions(+) diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index 7d4c597a60..459cf5e866 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/include/Api.hpp" #include +#include #include #include #include @@ -48,6 +49,23 @@ struct RequestState { }; std::array g_requests{}; std::int64_t g_nextId = 1; +std::atomic g_processingWaiters{0}; + +class ProcessingWaiter { +public: + ~ProcessingWaiter() { + if (observed) g_processingWaiters.fetch_sub(1); + } + + void Observe() { + if (observed) return; + g_processingWaiters.fetch_add(1); + observed = true; + } + +private: + bool observed = false; +}; RequestState* FindLocked(std::int32_t id) { if (id <= 0) return nullptr; @@ -222,6 +240,10 @@ int ValidateIds(const std::int32_t* id, std::int32_t num, const void* out) { extern "C" { +std::int32_t AioProcessingWaitersForTest_nid_no_patch() { + return g_processingWaiters.load(); +} + void AioAdvanceIdentityForTest_nid_no_patch(std::int64_t next) { std::lock_guard lock(g_mutex); if (next < g_nextId || next > static_cast(std::numeric_limits::max()) + 1) @@ -319,6 +341,7 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, if (error != 0) return error; if (mode != AioWaitAnd && mode != AioWaitOr) return SCE_KERNEL_ERROR_EINVAL; const auto start = std::chrono::steady_clock::now(); + ProcessingWaiter waiter; for (;;) { bool allDone = true; bool anyCompleted = false; @@ -334,6 +357,7 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, return 0; } } + waiter.Observe(); if (usec != nullptr && *usec != 0) { const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); if (elapsed > static_cast(*usec)) { @@ -349,6 +373,7 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; const auto start = std::chrono::steady_clock::now(); + ProcessingWaiter waiter; for (;;) { std::int32_t current; { @@ -359,6 +384,7 @@ int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec *state = current; return 0; } + waiter.Observe(); if (usec != nullptr && *usec != 0) { const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); if (elapsed > static_cast(*usec)) { diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index 59e45eacce..58f61d060f 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -187,6 +187,7 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec); int APS5_VABI sceKernelAioPollRequests(int32_t* id, int32_t num, int32_t* state); void AioAdvanceIdentityForTest_nid_no_patch(std::int64_t next); +std::int32_t AioProcessingWaitersForTest_nid_no_patch(); int APS5_VABI sceKernelAprResolveFilepathsToIds(const char** paths, uint32_t count, uint32_t* ids, uint32_t* errorIndex); int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* errorIndex); int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* errorIndex); diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 247a61420c..1cb4297940 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -217,6 +217,9 @@ static void CheckAioProcessingOwnership(bool fail) { Check(sceKernelAioWaitRequest(pending, &waited, nullptr) == 0); waiterDone.store(true); }); + const auto waiterDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (AioProcessingWaitersForTest_nid_no_patch() == 0 && std::chrono::steady_clock::now() < waiterDeadline) std::this_thread::yield(); + Check(AioProcessingWaitersForTest_nid_no_patch() == 1); const auto concurrent = SubmitEmptyAio(); Check(!waiterDone.load() && result.state == 7 && submittedId == -7); { @@ -228,6 +231,7 @@ static void CheckAioProcessingOwnership(bool fail) { waiter.join(); GuestArena::GuestArenaSetHostReadHook_nid_no_patch(nullptr); Check(waiterDone.load() && submitFailed == fail); + Check(AioProcessingWaitersForTest_nid_no_patch() == 0); if (fail) { Check(waited == SCE_KERNEL_ERROR_ESRCH && submittedId == -7 && result.state == 7); } else { From 64ad39e457b3ba7ecf86a2c17a26b4849d03bcc0 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 02:12:00 -0300 Subject: [PATCH 047/702] test(libkernel): reproduce cancellation waiting on thread exit --- core/libs/prx/libkernel/CMakeLists.txt | 19 +++ .../tests/GuestPthreadCancelInactiveWait.cpp | 153 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 core/libs/tests/GuestPthreadCancelInactiveWait.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9da2728d50..f081836d5e 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -867,6 +867,25 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_cancel PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_cancel_inactive_wait_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadCancelInactiveWait.cpp) + target_include_directories(guest_pthread_cancel_inactive_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_cancel_inactive_wait_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_cancel_inactive_wait_tests) + foreach(waitState IN ITEMS cleanup destructor) + foreach(waitMode IN ITEMS untimed timed) + foreach(requestApi IN ITEMS sce posix) + set(inactiveWaitTest guest_pthread_cancel_inactive_wait_${waitState}_${waitMode}_${requestApi}) + add_test(NAME ${inactiveWaitTest} COMMAND guest_pthread_cancel_inactive_wait_tests ${waitState} ${waitMode} ${requestApi}) + add_test(NAME ${inactiveWaitTest}_coarse COMMAND guest_pthread_cancel_inactive_wait_tests ${waitState} ${waitMode} ${requestApi}) + set_tests_properties(${inactiveWaitTest} ${inactiveWaitTest}_coarse PROPERTIES TIMEOUT 20) + set_tests_properties(${inactiveWaitTest}_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + if(WIN32) + set_tests_properties(${inactiveWaitTest} ${inactiveWaitTest}_coarse PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + endforeach() add_test_executable(guest_pthread_cancel_recovery_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadCancelRecovery.cpp) target_include_directories(guest_pthread_cancel_recovery_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_cancel_recovery_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestPthreadCancelInactiveWait.cpp b/core/libs/tests/GuestPthreadCancelInactiveWait.cpp new file mode 100644 index 0000000000..ed8a0aa083 --- /dev/null +++ b/core/libs/tests/GuestPthreadCancelInactiveWait.cpp @@ -0,0 +1,153 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: requirement failed\n", location.file_name(), location.line()); + std::_Exit(EXIT_FAILURE); +} + +template +static void Await(TPredicate predicate) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!predicate() && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(predicate()); +} + +struct Gate { + std::mutex mutex; + std::condition_variable changed; + bool entered = false; + bool released = false; + + void Park() { + std::unique_lock lock(mutex); + entered = true; + changed.notify_all(); + changed.wait(lock, [&] { return released; }); + } + + bool Entered() { + std::lock_guard lock(mutex); + return entered; + } + + void Release() { + std::lock_guard lock(mutex); + released = true; + changed.notify_all(); + } +}; + +struct GatedLock { + std::mutex mutex; + Gate gate; + bool first = true; + + void lock() { mutex.lock(); } + void unlock() { + mutex.unlock(); + if (first) { + first = false; + gate.Park(); + } + } +}; + +struct Context { + GatedLock lock; + TimedWait::Condition condition; + PthreadKey key = 0; + bool timed = false; + bool destructorWait = false; + bool ready = false; + std::atomic waitReturned{false}; + std::atomic returned{false}; + std::atomic cleanup{0}; + std::atomic destructors{0}; +}; + +static void Wait(Context& context) { + { + std::unique_lock lock(context.lock); + const auto deadline = context.timed ? std::optional(TimedWait::DeadlineNanos(60000000)) : std::nullopt; + while (!context.ready) Require(ThreadCancel::Wait(context.condition, lock, deadline)); + } + context.waitReturned = true; + ThreadCancel::Check(); +} + +static void APS5_VABI DestroySpecific(void* argument) { + auto& context = *static_cast(argument); + if (context.destructorWait) Wait(context); + Require(context.waitReturned && context.cleanup == 1); + ++context.destructors; +} + +static void APS5_VABI Cleanup(void* argument) { + auto& context = *static_cast(argument); + Require(context.cleanup.fetch_add(1) == 0); + if (!context.destructorWait) Wait(context); +} + +static void* APS5_VABI Worker(void* argument) { + auto& context = *static_cast(argument); + Require(scePthreadSetspecific(context.key, &context) == 0); + CleanupHandler cleanup{}; + __pthread_cleanup_push_imp_nid_postfix(Cleanup, &context, &cleanup); + scePthreadExit(&context); + context.returned = true; + return nullptr; +} + +static void Check(bool timed, bool destructorWait, bool posix) { + Context context; + context.timed = timed; + context.destructorWait = destructorWait; + Require(scePthreadKeyCreate(&context.key, reinterpret_cast(DestroySpecific)) == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Worker, &context, nullptr) == 0); + Await([&] { return context.lock.gate.Entered(); }); + std::atomic cancelReturned{false}; + std::thread cancel([&] { + for (int repeat = 0; repeat < 3; ++repeat) + Require((posix ? pthread_cancel_nid_postfix(thread) : scePthreadCancel(thread)) == 0); + cancelReturned = true; + }); + Await([&] { return cancelReturned.load(); }); + Require(!context.waitReturned && !context.returned && context.destructors == 0); + Require(context.cleanup == 1); + Require(thread->cancelPending.load()); + cancel.join(); + context.lock.gate.Release(); + { + std::lock_guard lock(context.lock.mutex); + context.ready = true; + context.condition.NotifyOne(); + } + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == 0); + Require(result == &context); + Require(context.waitReturned && !context.returned && context.cleanup == 1 && context.destructors == 1); + Require(scePthreadKeyDelete(context.key) == 0); +} + +int main(int count, char** arguments) { + Require(count == 4); + const std::string_view state(arguments[1]), mode(arguments[2]), api(arguments[3]); + Require(state == "cleanup" || state == "destructor"); + Require(mode == "untimed" || mode == "timed"); + Require(api == "sce" || api == "posix"); + Check(mode == "timed", state == "destructor", api == "posix"); +} From 84340f6d6391eb4e60d61a864f35a261ec1477e0 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 02:13:36 -0300 Subject: [PATCH 048/702] fix(libkernel): avoid cancellation wakeups during thread exit --- core/libs/prx/libkernel/Pthread/include/Pthread.hpp | 1 + core/libs/prx/libkernel/Pthread/src/Thread.cpp | 10 ++++++---- docs/dev/debt/libkernel.md | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 0e83376197..fa5b2db829 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -160,6 +160,7 @@ struct PthreadPrivate { std::atomic cancelPending{false}; std::mutex cancelLock; TimedWait::Condition* cancelWait = nullptr; + bool cancelWaitExiting = false; explicit PthreadPrivate(const PthreadAttrPrivate* attr = nullptr); ~PthreadPrivate(); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index d4505b8d1b..161818cb13 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -728,9 +728,11 @@ void ThreadCancel::CleanupRestore(void* mark) { } void ThreadCancel::BeginWait(TimedWait::Condition* condition) { - if (!CurrentGuestState().current) return; - std::lock_guard lock(CurrentGuestState().current->cancelLock); - CurrentGuestState().current->cancelWait = condition; + auto& state = CurrentGuestState(); + if (!state.current) return; + std::lock_guard lock(state.current->cancelLock); + state.current->cancelWait = condition; + state.current->cancelWaitExiting = condition && state.exiting; } void ThreadCancel::EndWait() { @@ -1198,7 +1200,7 @@ int APS5_VABI scePthreadCancel(Pthread thread) { for (;;) { { std::lock_guard lock(thread->cancelLock); - if (!thread->cancelWait) return SCE_OK; + if (!thread->cancelWait || thread->cancelWaitExiting) return SCE_OK; thread->cancelWait->NotifyAll(); } scePthreadYield(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index fd41607f44..3e18d10c61 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -189,6 +189,7 @@ - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest storage location for the life of the process; a guest that joins into a fresh location every time keeps every one. A handle copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over the original storage releases an object the copy still points at, and the copy reads freed memory. A detached thread is released when it ends, so joining it after it ended reads freed memory. A null entry crashed the payload and throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. PPSA34015 once stopped with `Joining guest thread: Access is denied` before the join and detach checks followed the console - [scePthreadCreate and scePthreadDetach, measured](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60): a name of 32 bytes or more returns `ENAMETOOLONG` (`0x8002003f`) without creating a thread, 31 is kept whole, and a null name gives an empty one; detaching a joined thread returns `ESRCH`; detaching a thread another thread is joining, or a detached one, returns `EINVAL`, as does joining a detached one; a second concurrent join returns `EOPNOTSUPP`; a null thread returns `EINVAL` from both; a null handle pointer to `scePthreadCreate` faulted and throws here. Whether the handle is written before the name check was not measured; here it is left alone - [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. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. +- [Cancellation wait registration](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - cancellation of a wait registered before thread exit still waits for the target to unregister, including while cancellation is disabled. An exception handler blocking that target, or cancelling itself inside the interrupted wait, can prevent the requester from returning. Nested waits overwrite the single registration without restoring the outer wait. Runtime-created threads exit without C++ stack unwinding, so registration cleanup cannot rely on destructors alone. These host progress limitations are not console measurements. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. 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. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. 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 - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified From f68eb887c91aaf20767ef327f367b9f21b264383 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 02:17:27 -0300 Subject: [PATCH 049/702] docs(libkernel): record wait allocation failure ownership gap --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 3e18d10c61..5ec4df06ef 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -189,7 +189,7 @@ - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest storage location for the life of the process; a guest that joins into a fresh location every time keeps every one. A handle copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over the original storage releases an object the copy still points at, and the copy reads freed memory. A detached thread is released when it ends, so joining it after it ended reads freed memory. A null entry crashed the payload and throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. PPSA34015 once stopped with `Joining guest thread: Access is denied` before the join and detach checks followed the console - [scePthreadCreate and scePthreadDetach, measured](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60): a name of 32 bytes or more returns `ENAMETOOLONG` (`0x8002003f`) without creating a thread, 31 is kept whole, and a null name gives an empty one; detaching a joined thread returns `ESRCH`; detaching a thread another thread is joining, or a detached one, returns `EINVAL`, as does joining a detached one; a second concurrent join returns `EOPNOTSUPP`; a null thread returns `EINVAL` from both; a null handle pointer to `scePthreadCreate` faulted and throws here. Whether the handle is written before the name check was not measured; here it is left alone - [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. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. -- [Cancellation wait registration](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - cancellation of a wait registered before thread exit still waits for the target to unregister, including while cancellation is disabled. An exception handler blocking that target, or cancelling itself inside the interrupted wait, can prevent the requester from returning. Nested waits overwrite the single registration without restoring the outer wait. Runtime-created threads exit without C++ stack unwinding, so registration cleanup cannot rely on destructors alone. These host progress limitations are not console measurements. +- [Cancellation wait registration](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - cancellation of a wait registered before thread exit still waits for the target to unregister, including while cancellation is disabled. A blocked handler or self-cancellation inside the interrupted wait can prevent the requester from returning. Nested waits do not restore the outer registration. If Windows waiter allocation throws after registration, `cancelWait` remains set and a join retains its `_joining` claim. Runtime-created thread exit skips C++ stack unwinding, so destructors alone cannot clean up registrations. These host progress limitations are not console measurements. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. 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. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. 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 - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified From ba62e160e47c970791ff4313346e2ea527d7d050 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sun, 25 Oct 2026 05:46:21 +0200 Subject: [PATCH 050/702] test(agc): wait for draw submission trace publication Wait for the exact completed confirmation before inspecting submitted draw tokens. Preserve the existing assertions and inspect the completed trace for failures. Fixes #10835 AI-assisted: yes (Codex) --- .../tests/execution/DrawShaderFailure.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawShaderFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawShaderFailure.cpp index 2051585c2d..542cb8c3a4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawShaderFailure.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawShaderFailure.cpp @@ -297,11 +297,12 @@ void SuccessfulDrawEmitsNoFailureRecord(const char* tracePath) { const auto recorded = TraceLine(ReadTraceFile(tracePath), "draw-recorded token=" + token); const auto batch = TraceField(recorded, "batch"); Require(!batch.empty() && batch != "0", "the draw-recorded record carries no actual batch serial"); - Require(ReadTraceFile(tracePath).find("submit batch=" + batch + " ") != std::string::npos, "the recorded batch was not submitted"); - const auto submit = TraceLine(ReadTraceFile(tracePath), "submit batch=" + batch + " "); + Require(WaitForTraceRecord(tracePath, "submit-confirmed batch=" + batch + " tokens=[" + token + "]\n"), "the confirmed submit record lost the draw token set"); + const auto completedTrace = ReadTraceFile(tracePath); + Require(completedTrace.find("submit batch=" + batch + " ") != std::string::npos, "the recorded batch was not submitted"); + const auto submit = TraceLine(completedTrace, "submit batch=" + batch + " "); Require(submit == "submit batch=" + batch + " tokens=[" + token + "]", "the submit record lost the draw token set"); - Require(ReadTraceFile(tracePath).find("submit-confirmed batch=" + batch + " tokens=[" + token + "]") != std::string::npos, "the confirmed submit record lost the draw token set"); - Require(trace.find("draw-shader-request-failed") == std::string::npos, "a successful draw emitted a draw-shader-request-failed record"); + Require(completedTrace.find("draw-shader-request-failed") == std::string::npos, "a successful draw emitted a draw-shader-request-failed record"); } void DisabledCorrelationTraceLeavesDrawBehaviorUnchanged(const char* tracePath) { From a9ab9a781e587ba9fae90717730efa326c1ff487 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:13:47 +0000 Subject: [PATCH 051/702] fix(tools): reject malformed trace events in trace_digest --- tools/trace_digest.py | 54 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 7998b670bb..a46167a034 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -4,6 +4,7 @@ import collections import gzip import html import json +import math import sys import zlib from pathlib import Path @@ -26,6 +27,34 @@ def is_wait(name): return "wait" in name.lower() +def _event_value(value, label, index): + try: + number = float(value) + except (TypeError, ValueError, OverflowError): + raise ValueError(f"trace event {index}: {label} must be a finite number") from None + if not math.isfinite(number) or abs(number) >= 1e300: + raise ValueError(f"trace event {index}: {label} must be a finite number") + return number + + +def _event_number(event, key, index): + return _event_value(event.get(key), key, index) + + +def _event_tid(event, index): + tid = event.get("tid") + if isinstance(tid, bool) or not isinstance(tid, int): + raise ValueError(f"trace event {index}: tid must be an integer") + return tid + + +def _event_args(event, index): + args = event.get("args", {}) + if not isinstance(args, dict): + raise ValueError(f"trace event {index}: args must be an object") + return args + + class Trace: def __init__(self, document): self.threads = {} @@ -43,16 +72,29 @@ class Trace: raise ValueError(f"trace event {index} must be an object") phase = event.get("ph") if phase == "M" and event.get("name") == "thread_name": - self.threads[event["tid"]] = event["args"]["name"] + tid = _event_tid(event, index) + name = _event_args(event, index).get("name") + if not isinstance(name, str): + raise ValueError(f"trace event {index}: args.name must be a string") + self.threads[tid] = name elif phase == "X": - self.slices[event["tid"]].append([float(event["ts"]), float(event["dur"]), event["name"], 0, False]) - batch = event.get("args", {}).get("batch") + tid = _event_tid(event, index) + name = event.get("name") + if not isinstance(name, str): + raise ValueError(f"trace event {index}: name must be a string") + ts = _event_number(event, "ts", index) + dur = _event_number(event, "dur", index) + self.slices[tid].append([ts, dur, name, 0, False]) + batch = _event_args(event, index).get("batch") if batch: - self.batched.append((event["tid"], float(event["ts"]), float(event["dur"]), event["name"], batch)) + if isinstance(batch, bool) or not isinstance(batch, (int, str)): + raise ValueError(f"trace event {index}: args.batch must be an integer or a string") + self.batched.append((tid, ts, dur, name, batch)) elif phase == "i" and event.get("name") == "present": - self.presents.append((float(event["ts"]), event.get("args", {}).get("present"))) + self.presents.append((_event_number(event, "ts", index), _event_args(event, index).get("present"))) elif phase == "C": - self.folded[event["tid"]] += sum(float(value) for value in event.get("args", {}).values()) + tid = _event_tid(event, index) + self.folded[tid] += sum(_event_value(value, "args value", index) for value in _event_args(event, index).values()) self.presents.sort() for slices in self.slices.values(): slices.sort(key=lambda entry: (entry[0], -entry[1])) From 1e351c81e0cd51fb523ec39acf13f7c14c4c711b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:13:47 +0000 Subject: [PATCH 052/702] test(tools): cover malformed trace event fields in trace_digest --- tools/tests/test_trace_digest.py | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 9e5f26c1bc..b1a20e8b0f 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -66,6 +66,48 @@ class TraceDigestTests(unittest.TestCase): self.assertNotIn("Traceback", result.stderr) self.assertEqual(result.stdout, "") + def test_cli_rejects_invalid_event_fields_without_partial_output(self): + slice_event = complete(1, "Driver.Packet", 0, 10) + cases = [ + (dict(slice_event, tid=None), "trace event 0: tid must be an integer"), + (dict(slice_event, tid="1"), "trace event 0: tid must be an integer"), + (dict(slice_event, tid=True), "trace event 0: tid must be an integer"), + ({key: value for key, value in slice_event.items() if key != "tid"}, "trace event 0: tid must be an integer"), + ({key: value for key, value in slice_event.items() if key != "dur"}, "trace event 0: dur must be a finite number"), + (dict(slice_event, dur=None), "trace event 0: dur must be a finite number"), + (dict(slice_event, dur="slow"), "trace event 0: dur must be a finite number"), + (dict(slice_event, dur=[1]), "trace event 0: dur must be a finite number"), + (dict(slice_event, ts={}), "trace event 0: ts must be a finite number"), + (dict(slice_event, ts=float("nan")), "trace event 0: ts must be a finite number"), + (dict(slice_event, ts=float("inf")), "trace event 0: ts must be a finite number"), + (dict(slice_event, ts=1e308), "trace event 0: ts must be a finite number"), + (dict(slice_event, name=7), "trace event 0: name must be a string"), + ({key: value for key, value in slice_event.items() if key != "name"}, "trace event 0: name must be a string"), + (dict(slice_event, args=[]), "trace event 0: args must be an object"), + (dict(slice_event, args={"batch": [1]}), "trace event 0: args.batch must be an integer or a string"), + ({"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 1}, "trace event 0: args.name must be a string"), + ({"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 1, "args": {}}, "trace event 0: args.name must be a string"), + ({"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": [1], "args": {"name": "a"}}, "trace event 0: tid must be an integer"), + ({"name": "present", "ph": "i", "ts": None, "pid": 7, "tid": 2, "s": "g"}, "trace event 0: ts must be a finite number"), + ({"name": "present", "ph": "i", "pid": 7, "tid": 2, "s": "g"}, "trace event 0: ts must be a finite number"), + ({"name": "present", "ph": "i", "ts": 0, "pid": 7, "tid": 2, "s": "g", "args": 5}, "trace event 0: args must be an object"), + ({"name": "folded", "ph": "C", "ts": 0, "pid": 7, "args": {"ms": 1.5}}, "trace event 0: tid must be an integer"), + ({"name": "folded", "ph": "C", "ts": 0, "pid": 7, "tid": 1, "args": []}, "trace event 0: args must be an object"), + ({"name": "folded", "ph": "C", "ts": 0, "pid": 7, "tid": 1, "args": {"ms": "x"}}, "trace event 0: args value must be a finite number"), + ] + for event, message in cases: + for compressed in (False, True): + with self.subTest(event=event, compressed=compressed): + path = Path(self.directory.name) / ("fields.json.gz" if compressed else "fields.json") + data = json.dumps({"traceEvents": [event]}).encode("utf-8") + path.write_bytes(gzip.compress(data) if compressed else data) + result = subprocess.run([sys.executable, trace_digest.__file__, str(path), "--json"], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("FAIL: " + message, result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + def test_cli_reports_unreadable_inputs_without_partial_output(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) From 8a173193c1773be175868dd033f8cb1ced1a705d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 05:45:58 +0000 Subject: [PATCH 053/702] fix(tools): validate falsey and nonnumeric trace fields --- tools/tests/test_trace_digest.py | 6 ++++++ tools/trace_digest.py | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index b1a20e8b0f..2efe42976c 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -76,8 +76,11 @@ class TraceDigestTests(unittest.TestCase): ({key: value for key, value in slice_event.items() if key != "dur"}, "trace event 0: dur must be a finite number"), (dict(slice_event, dur=None), "trace event 0: dur must be a finite number"), (dict(slice_event, dur="slow"), "trace event 0: dur must be a finite number"), + (dict(slice_event, dur="10"), "trace event 0: dur must be a finite number"), + (dict(slice_event, dur=True), "trace event 0: dur must be a finite number"), (dict(slice_event, dur=[1]), "trace event 0: dur must be a finite number"), (dict(slice_event, ts={}), "trace event 0: ts must be a finite number"), + (dict(slice_event, ts="0"), "trace event 0: ts must be a finite number"), (dict(slice_event, ts=float("nan")), "trace event 0: ts must be a finite number"), (dict(slice_event, ts=float("inf")), "trace event 0: ts must be a finite number"), (dict(slice_event, ts=1e308), "trace event 0: ts must be a finite number"), @@ -85,6 +88,8 @@ class TraceDigestTests(unittest.TestCase): ({key: value for key, value in slice_event.items() if key != "name"}, "trace event 0: name must be a string"), (dict(slice_event, args=[]), "trace event 0: args must be an object"), (dict(slice_event, args={"batch": [1]}), "trace event 0: args.batch must be an integer or a string"), + (dict(slice_event, args={"batch": []}), "trace event 0: args.batch must be an integer or a string"), + (dict(slice_event, args={"batch": None}), "trace event 0: args.batch must be an integer or a string"), ({"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 1}, "trace event 0: args.name must be a string"), ({"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 1, "args": {}}, "trace event 0: args.name must be a string"), ({"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": [1], "args": {"name": "a"}}, "trace event 0: tid must be an integer"), @@ -94,6 +99,7 @@ class TraceDigestTests(unittest.TestCase): ({"name": "folded", "ph": "C", "ts": 0, "pid": 7, "args": {"ms": 1.5}}, "trace event 0: tid must be an integer"), ({"name": "folded", "ph": "C", "ts": 0, "pid": 7, "tid": 1, "args": []}, "trace event 0: args must be an object"), ({"name": "folded", "ph": "C", "ts": 0, "pid": 7, "tid": 1, "args": {"ms": "x"}}, "trace event 0: args value must be a finite number"), + ({"name": "folded", "ph": "C", "ts": 0, "pid": 7, "tid": 1, "args": {"ms": True}}, "trace event 0: args value must be a finite number"), ] for event, message in cases: for compressed in (False, True): diff --git a/tools/trace_digest.py b/tools/trace_digest.py index a46167a034..77e1448d19 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -28,6 +28,8 @@ def is_wait(name): def _event_value(value, label, index): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError(f"trace event {index}: {label} must be a finite number") try: number = float(value) except (TypeError, ValueError, OverflowError): @@ -85,8 +87,9 @@ class Trace: ts = _event_number(event, "ts", index) dur = _event_number(event, "dur", index) self.slices[tid].append([ts, dur, name, 0, False]) - batch = _event_args(event, index).get("batch") - if batch: + args = _event_args(event, index) + if "batch" in args: + batch = args["batch"] if isinstance(batch, bool) or not isinstance(batch, (int, str)): raise ValueError(f"trace event {index}: args.batch must be an integer or a string") self.batched.append((tid, ts, dur, name, batch)) From 0b035b036c9d8ec022970b704ecbf1a2e21a6e92 Mon Sep 17 00:00:00 2001 From: Mouhand Kaddo <73709175+Mouhand-Kaddo@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:37:36 +0400 Subject: [PATCH 054/702] fix(agc): keep the committed-compare chunk in HostThreadLocal The Windows compare buffer in equalsCommittedBytes was a raw thread_local vector, whose destructor runs after emulated TLS has freed it in a PRX on Windows. It now lives in HostThreadLocal and keeps its capacity between calls. RunVanishedPageReadTests now requires the guest arena instead of returning silently when it is unavailable. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp | 5 ++++- core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 0d46f39cf9..1be0ebcc82 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -8,6 +8,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libc/include/HostThreadSlot.hpp" #include #include @@ -2581,7 +2582,9 @@ void copyCommittedBytes(std::byte* destination, std::uint64_t source, std::size_ bool equalsCommittedBytes(std::uint64_t address, const std::byte* expected, std::size_t bytes) { #ifdef _WIN32 constexpr std::size_t ChunkBytes = 64 * PageBytes; - thread_local std::vector current(ChunkBytes); + struct CompareChunkStorage {}; + auto& current = HostThreadLocal, CompareChunkStorage>(); + if (current.size() < ChunkBytes) current.resize(ChunkBytes); for (std::size_t done = 0; done < bytes; done += ChunkBytes) { const auto count = std::min(ChunkBytes, bytes - done); bool equal = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index 8d8a41e94a..6a1deede07 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -333,7 +333,7 @@ void RunUnmappedGapTests() { void RunVanishedPageReadTests() { #ifdef _WIN32 namespace GuestMemory = AgcDriver::GuestMemory; - if (!GuestArena::GuestArenaAvailable_nid_postfix()) return; + Require(GuestArena::GuestArenaAvailable_nid_postfix(), "released-page read fixture needs the guest arena"); constexpr std::size_t page = 4096; constexpr std::uint32_t ReadWriteProtection = 0x04; auto* block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * page, page)); From cb92e51d72ce8aa06dbd87b80c3f307cff4b04b2 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sat, 24 Oct 2026 10:40:59 +0200 Subject: [PATCH 055/702] fix(libSceAudioOut): skip the SDL device open when no output device exists With no audio endpoint, SDL's WASAPI backend retries the default device for 8 s before it fails. Every port open paid that before falling back to virtual pacing. Ask SDL for the output device count first and take the virtual path at once when it is zero. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 2 +- core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 4459dc8152..1ce2725aa0 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -142,7 +142,7 @@ static bool ensureSdlAudio() { } static bool openDevice(Port& port) { - if (!ensureSdlAudio()) { + if (!ensureSdlAudio() || SDL_GetNumAudioDevices(0) == 0) { return false; } SDL_AudioSpec desired{}; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 905eaa42f5..f9d4c98b41 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -108,6 +108,10 @@ static void OpenDevice(AudioOut2Context& context) { AUDIOOUT2_TRACE("SDL audio init failed: %s\n", SDL_GetError()); return; } + if (SDL_GetNumAudioDevices(0) == 0) { + AUDIOOUT2_TRACE("SDL reports no audio output device\n"); + return; + } SDL_AudioSpec desired{}; desired.freq = static_cast(AUDIO_OUT2_SAMPLE_RATE); desired.format = AUDIO_F32SYS; From 41b8afb06076894cb22a1ba3b5ea384af2c40c9e Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sun, 25 Oct 2026 10:17:11 +0100 Subject: [PATCH 056/702] fix(libSceAudioOut): skip the default open only when WASAPI reports no endpoint SDL documents that a default open can succeed when SDL_GetNumAudioDevices returns zero, for instance ALSA whose default PCM works without device hints. The shortcut now applies only to the wasapi driver, which enumerates active render endpoints synchronously when the subsystem starts; with none, its default open has no endpoint to use and fails after SDL's 8 s retry. audio_out_default_open runs AudioOut.cpp against a stubbed SDL in four modes: zero devices on alsa with a working default, zero on wasapi, a positive count and an unknown count. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 15 ++++ .../libSceAudioOut/include/AudioOutSdl.hpp | 6 ++ core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 2 +- .../libSceAudioOut/src/AudioOut2Context.cpp | 4 +- .../tests/AudioOutDefaultOpen.cpp | 74 +++++++++++++++++++ 5 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutDefaultOpen.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 7c97d07a55..2b88a99768 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -47,6 +47,21 @@ if(BUILD_TESTING) endif() endforeach() + add_test_executable(audio_out_default_open_tests tests/AudioOutDefaultOpen.cpp src/AudioOut.cpp src/AudioOut2PadMix.cpp src/AudioOutShutdown.cpp) + target_compile_definitions(audio_out_default_open_tests PRIVATE SDL_MAIN_HANDLED) + target_include_directories(audio_out_default_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(audio_out_default_open_tests PRIVATE libc libkernel libScePad) + configure_windows_unwind(audio_out_default_open_tests) + foreach(defaultOpenMode IN ITEMS zero_default no_endpoint positive unknown) + set(defaultOpenTest "audio_out_default_open_${defaultOpenMode}") + add_test(NAME ${defaultOpenTest} COMMAND audio_out_default_open_tests ${defaultOpenMode}) + set_tests_properties(${defaultOpenTest} PROPERTIES TIMEOUT 5) + if(WIN32) + set_tests_properties(${defaultOpenTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(audio_out_buffer_size_tests tests/AudioOutBufferSize.cpp src/AudioOut.cpp src/AudioOut2PadMix.cpp src/AudioOutShutdown.cpp) target_compile_definitions(audio_out_buffer_size_tests PRIVATE SDL_MAIN_HANDLED) target_include_directories(audio_out_buffer_size_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp index f0e8c87cc4..3d38a0b7c5 100644 --- a/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp +++ b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp @@ -1,6 +1,12 @@ #ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_INCLUDE_AUDIOOUTSDL_HPP #define CORE_LIBS_PRX_LIBSCEAUDIOOUT_INCLUDE_AUDIOOUTSDL_HPP +#include + int InitializeSdlAudio_nid_no_patch(); +inline bool SdlAudioReportsNoOutputEndpoint(const char* driver, int outputDevices) { + return outputDevices == 0 && driver != nullptr && std::strcmp(driver, "wasapi") == 0; +} + #endif diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 1ce2725aa0..d0013405ec 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -142,7 +142,7 @@ static bool ensureSdlAudio() { } static bool openDevice(Port& port) { - if (!ensureSdlAudio() || SDL_GetNumAudioDevices(0) == 0) { + if (!ensureSdlAudio() || SdlAudioReportsNoOutputEndpoint(SDL_GetCurrentAudioDriver(), SDL_GetNumAudioDevices(0))) { return false; } SDL_AudioSpec desired{}; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index f9d4c98b41..03f55dd244 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -108,8 +108,8 @@ static void OpenDevice(AudioOut2Context& context) { AUDIOOUT2_TRACE("SDL audio init failed: %s\n", SDL_GetError()); return; } - if (SDL_GetNumAudioDevices(0) == 0) { - AUDIOOUT2_TRACE("SDL reports no audio output device\n"); + if (SdlAudioReportsNoOutputEndpoint(SDL_GetCurrentAudioDriver(), SDL_GetNumAudioDevices(0))) { + AUDIOOUT2_TRACE("SDL reports no WASAPI output endpoint\n"); return; } SDL_AudioSpec desired{}; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutDefaultOpen.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutDefaultOpen.cpp new file mode 100644 index 0000000000..1a0ff32dff --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutDefaultOpen.cpp @@ -0,0 +1,74 @@ +#include "prx/libSceAudioOut/include/AudioOut.hpp" +#include "SDL.h" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +namespace { +constexpr SDL_AudioDeviceID device = 7; +const char* driver = "wasapi"; +int outputDevices = 0; +int opened = 0; +int closed = 0; + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} +} + +int InitializeSdlAudio_nid_no_patch() { return 0; } +extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } +extern "C" int SDLCALL SDL_GetNumAudioDevices(int) { return outputDevices; } +extern "C" const char* SDLCALL SDL_GetAudioDeviceName(int, int) { return "Main speakers"; } +extern "C" int SDLCALL SDL_GetAudioDeviceSpec(int, int, SDL_AudioSpec* spec) { *spec = {}; spec->channels = 2; return 0; } +extern "C" const char* SDLCALL SDL_GetCurrentAudioDriver() { return driver; } +extern "C" const char* SDLCALL SDL_GetError() { return "test error"; } +extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char* name, int capture, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int) { + Require(name == nullptr && capture == 0, "the main port did not open the default output"); + ++opened; + *obtained = *desired; + return device; +} +extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID id, int) { Require(id == device, "wrong device paused"); } +extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID id) { Require(id == device, "wrong queue queried"); return 0; } +extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID id, const void*, Uint32) { Require(id == device, "wrong queue written"); return 0; } +extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID id) { Require(id == device, "wrong queue cleared"); } +extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID id) { Require(id == device, "wrong device closed"); ++closed; } + +int main(int argc, char** argv) { + try { + Require(argc == 2, "invalid test arguments"); + const std::string_view mode = argv[1]; + bool expectOpen = true; + if (mode == "zero_default") { + driver = "alsa"; + outputDevices = 0; + } else if (mode == "no_endpoint") { + driver = "wasapi"; + outputDevices = 0; + expectOpen = false; + } else if (mode == "positive") { + driver = "wasapi"; + outputDevices = 2; + } else if (mode == "unknown") { + driver = "wasapi"; + outputDevices = -1; + } else { + Require(false, "invalid default open mode"); + } + const int handle = sceAudioOutOpen(0x10000000, 0, 0, 256, 48000, 1); + Require(handle >= 1, "the main port did not open"); + Require(opened == (expectOpen ? 1 : 0), expectOpen ? "the default output was not opened" : "the default output was opened with no WASAPI endpoint"); + Require(sceAudioOutClose(handle) == 0, "the main port did not close"); + Require(closed == opened, "the default output was not closed with its port"); + Require(SdlAudioReportsNoOutputEndpoint(driver, outputDevices) == !expectOpen, "the no-endpoint predicate disagrees with the mode"); + Require(!SdlAudioReportsNoOutputEndpoint(nullptr, 0), "a missing driver name skipped the default open"); + std::puts("default output open tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From e63a51c93a0e17d1f78dc1181a0ff77d0c4209b3 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sun, 25 Oct 2026 10:50:38 +0100 Subject: [PATCH 057/702] fix(libSceAudioOut): confirm the missing WASAPI endpoint before skipping the open SDL's WASAPI enumeration drops failures silently: EnumAudioEndpoints or GetCount errors leave the list empty, and a device whose metadata lookup fails is left out, so an empty list does not prove there is no default endpoint. The skip now also requires IMMDeviceEnumerator's GetDefaultAudioEndpoint(eRender, eConsole), the call SDL's default open makes, to return ERROR_NOT_FOUND. The probe runs only when the wasapi list is empty. audio_out_default_open gains wasapi_zero_default (empty WASAPI list, the probe finds an endpoint, the default is opened) and checks that the probe runs only for an empty WASAPI list. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 2 +- .../libSceAudioOut/include/AudioOutSdl.hpp | 3 ++- .../prx/libSceAudioOut/src/AudioOutSdl.cpp | 21 +++++++++++++++++++ .../tests/AudioOutBufferSize.cpp | 1 + .../tests/AudioOutDefaultOpen.cpp | 12 +++++++++++ .../tests/AudioOutVibrationDevice.cpp | 1 + 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 2b88a99768..d352122663 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -52,7 +52,7 @@ if(BUILD_TESTING) target_include_directories(audio_out_default_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(audio_out_default_open_tests PRIVATE libc libkernel libScePad) configure_windows_unwind(audio_out_default_open_tests) - foreach(defaultOpenMode IN ITEMS zero_default no_endpoint positive unknown) + foreach(defaultOpenMode IN ITEMS zero_default wasapi_zero_default no_endpoint positive unknown) set(defaultOpenTest "audio_out_default_open_${defaultOpenMode}") add_test(NAME ${defaultOpenTest} COMMAND audio_out_default_open_tests ${defaultOpenMode}) set_tests_properties(${defaultOpenTest} PROPERTIES TIMEOUT 5) diff --git a/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp index 3d38a0b7c5..0d72f3578d 100644 --- a/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp +++ b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp @@ -4,9 +4,10 @@ #include int InitializeSdlAudio_nid_no_patch(); +bool WasapiDefaultOutputAbsent_nid_no_patch(); inline bool SdlAudioReportsNoOutputEndpoint(const char* driver, int outputDevices) { - return outputDevices == 0 && driver != nullptr && std::strcmp(driver, "wasapi") == 0; + return outputDevices == 0 && driver != nullptr && std::strcmp(driver, "wasapi") == 0 && WasapiDefaultOutputAbsent_nid_no_patch(); } #endif diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp b/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp index 45f41f5a43..4bec9dd7b6 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp @@ -2,9 +2,30 @@ #include "SDL.h" #include +#ifdef _WIN32 +#include +#endif int InitializeSdlAudio_nid_no_patch() { static std::mutex initializationMutex; std::lock_guard lock(initializationMutex); return SDL_InitSubSystem(SDL_INIT_AUDIO); } + +bool WasapiDefaultOutputAbsent_nid_no_patch() { +#ifdef _WIN32 + const HRESULT initialized = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + bool absent = false; + IMMDeviceEnumerator* enumerator = nullptr; + if (SUCCEEDED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)))) { + IMMDevice* device = nullptr; + absent = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device) == HRESULT_FROM_WIN32(ERROR_NOT_FOUND); + if (device != nullptr) device->Release(); + enumerator->Release(); + } + if (SUCCEEDED(initialized)) CoUninitialize(); + return absent; +#else + return false; +#endif +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp index 6b2eaaef4e..db9b3df26e 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp @@ -53,6 +53,7 @@ void RunOverflow(std::uint32_t frames, std::uint32_t format, std::uint32_t frame } int InitializeSdlAudio_nid_no_patch() { return 0; } +bool WasapiDefaultOutputAbsent_nid_no_patch() { return false; } extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } extern "C" int SDLCALL SDL_GetNumAudioDevices(int) { return 1; } extern "C" const char* SDLCALL SDL_GetAudioDeviceName(int, int) { return "Main speakers"; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutDefaultOpen.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutDefaultOpen.cpp index 1a0ff32dff..0b54f5b399 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutDefaultOpen.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutDefaultOpen.cpp @@ -10,6 +10,8 @@ namespace { constexpr SDL_AudioDeviceID device = 7; const char* driver = "wasapi"; int outputDevices = 0; +bool endpointAbsent = false; +int probes = 0; int opened = 0; int closed = 0; @@ -19,6 +21,10 @@ void Require(bool value, const char* message) { } int InitializeSdlAudio_nid_no_patch() { return 0; } +bool WasapiDefaultOutputAbsent_nid_no_patch() { + ++probes; + return endpointAbsent; +} extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } extern "C" int SDLCALL SDL_GetNumAudioDevices(int) { return outputDevices; } extern "C" const char* SDLCALL SDL_GetAudioDeviceName(int, int) { return "Main speakers"; } @@ -45,9 +51,13 @@ int main(int argc, char** argv) { if (mode == "zero_default") { driver = "alsa"; outputDevices = 0; + } else if (mode == "wasapi_zero_default") { + driver = "wasapi"; + outputDevices = 0; } else if (mode == "no_endpoint") { driver = "wasapi"; outputDevices = 0; + endpointAbsent = true; expectOpen = false; } else if (mode == "positive") { driver = "wasapi"; @@ -63,6 +73,8 @@ int main(int argc, char** argv) { Require(opened == (expectOpen ? 1 : 0), expectOpen ? "the default output was not opened" : "the default output was opened with no WASAPI endpoint"); Require(sceAudioOutClose(handle) == 0, "the main port did not close"); Require(closed == opened, "the default output was not closed with its port"); + Require(probes == (driver == std::string_view("wasapi") && outputDevices == 0 ? 1 : 0), "the default endpoint was probed outside an empty WASAPI list"); + probes = 0; Require(SdlAudioReportsNoOutputEndpoint(driver, outputDevices) == !expectOpen, "the no-endpoint predicate disagrees with the mode"); Require(!SdlAudioReportsNoOutputEndpoint(nullptr, 0), "a missing driver name skipped the default open"); std::puts("default output open tests passed"); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationDevice.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationDevice.cpp index 79681dff96..505cbf878c 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationDevice.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationDevice.cpp @@ -186,6 +186,7 @@ void RunCadence() { } int InitializeSdlAudio_nid_no_patch() { return failInitialize ? -1 : 0; } +bool WasapiDefaultOutputAbsent_nid_no_patch() { return false; } extern "C" void PadQueueLegacyAudioHaptics_nid_postfix(int source, std::uint64_t begin, std::uint64_t end, std::uint8_t large, std::uint8_t small) { blocks.push_back({source, begin, end, {large, small}}); timeline.Queue(source, begin, end, large, small); From 464c48b17bfadd1cb65d49fc3da7d09dcca49a37 Mon Sep 17 00:00:00 2001 From: thepoppfish <147332153+thepoppfish@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:49:21 +0200 Subject: [PATCH 058/702] fix(tools): require module suffixes for filename aliases --- tools/import_audit.py | 13 ++++++++----- tools/tests/test_import_audit.py | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 3e33d27568..312c768877 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -680,9 +680,8 @@ def module_stem(name, fold): name = name.lower() for suffix in MODULE_FILE_SUFFIXES: if len(name) > len(suffix) and name.endswith(suffix): - name = name[:-len(suffix)] - break - return name + return name[:-len(suffix)] + return None class ModuleNames(set): @@ -692,10 +691,14 @@ class ModuleNames(set): def __init__(self, names=(), fold=False): super().__init__(names) self.fold = fold - self.stems = {module_stem(name, fold) for name in self} + self.files = {name.lower() if fold else name for name in self} + self.stems = {stem for name in self if (stem := module_stem(name, fold)) is not None} def __contains__(self, library): - return isinstance(library, str) and module_stem(library, self.fold) in self.stems + if not isinstance(library, str): + return False + filename = library.lower() if self.fold else library + return filename in self.files or module_stem(library, self.fold) in self.stems def classify(nid, library, providers, stubs, modules, partial=frozenset()): diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 00fcfea96c..7138c1eb27 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -739,6 +739,31 @@ class ImportAuditTests(unittest.TestCase): record = next(record for record in data["imports"] if record["library"] == "shipping.debug_prx") self.assertEqual(record["class"], "module") + def test_module_suffix_aliases_require_suffixes_on_both_filenames(self): + work = self.work + registry, report = work.root / "game.registry.json", work.root / "audit.json" + cases = (("shipping.prx", "shipping"), ("shipping", "shipping.prx"), + ("shipping.data", "shipping.data.prx"), (".prx", ".sprx")) + for windows in (False, True): + (work.libs / "libc.prx").write_bytes(build_pe([]) if windows else exporting()) + for filename, alias in cases: + module = work.modules / filename + module.write_bytes(exporting("AAAAAAAAAAA")) + try: + for needed in (filename, alias): + with self.subTest(windows=windows, filename=filename, needed=needed): + write_registry(registry, [("AAAAAAAAAAA", needed)]) + import_audit.needed_registry_path(registry).write_text(json.dumps([needed]), encoding="utf-8") + code, _, err = work.run(registry, "--json", str(report)) + exact = needed == filename + self.assertEqual((code, err), (0 if exact else 1, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["missing_libraries"], {} if exact else {needed: 1}) + self.assertEqual(data["imports"][0]["class"], "module" if exact else "absent") + self.assertEqual(data["references"], 1) + finally: + module.unlink() + def test_module_names_ignore_case_for_windows_output_only(self): imports = [("AAAAAAAAAAA", "SDK-Shipping.debug_prx", False)] records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"}, fold=True)) From c8430e739c05e14c945024d8ca5a2501bbd04840 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:06:07 +0200 Subject: [PATCH 059/702] fix(libkernel): fail gettimeofday, getitimer, setitimer and the sceKernel time getters with EFAULT on an unusable pointer These wrote their result through the caller's pointer in place, or read the itimerval of setitimer without a check, so a pointer into unmapped memory crashed the host. Build the result locally and copy it out with the checked write (and read the setitimer value with a checked read), failing with EFAULT as FreeBSD 11.4 does: copyout in sys_gettimeofday (sys/kern/kern_time.c:646 and 651) and sys_getitimer (kern_time.c:745), copyin (kern_time.c:803) and copyout (kern_time.c:808) in sys_setitimer. getrlimit and getrusage are left to #8986 and #8988, which cover the same family for those two calls. Builds on fix/output-structs-efault, which adds GuestCopyOut_nid_no_patch. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/General.cpp | 31 ++++++++++ core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ .../libs/prx/libkernel/System/src/Signals.cpp | 21 +++++-- core/libs/prx/libkernel/Time/Time.cpp | 23 ++++++-- core/libs/tests/GuestOutputPointerFault.cpp | 57 +++++++++++++++++++ 6 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 core/libs/tests/GuestOutputPointerFault.cpp diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 4d4db4d068..17d627e327 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -31,6 +31,7 @@ extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& desti extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, std::size_t bytes); +extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, std::size_t bytes); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); extern "C" std::filesystem::path ResolveDiskPath_nid_no_patch(const char* path); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index e84d6caafc..e20870290f 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -837,6 +837,37 @@ extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, #endif } +extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + const auto start = reinterpret_cast(source); + if (start == 0 || bytes > UINTPTR_MAX - start) return false; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); +#ifdef _WIN32 + for (auto address = start; address < start + bytes;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory) || memory.State != MEM_COMMIT || + (memory.Protect & PAGE_GUARD) != 0 || + (memory.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) == 0) return false; + address = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + std::memcpy(destination, source, bytes); + return true; +#elif defined(__linux__) + iovec local{destination, bytes}; + iovec remote{const_cast(source), bytes}; + const ssize_t copied = ::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0) { + if (errno == EFAULT) return false; + std::memcpy(destination, source, bytes); + return true; + } + return static_cast(copied) == bytes; +#else + std::memcpy(destination, source, bytes); + return true; +#endif +} + extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { auto resolved = ResolveLocked(path, false, true); AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9da2728d50..09349c46a3 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1875,6 +1875,16 @@ if(BUILD_TESTING) set_tests_properties(guest_file_vector_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_output_pointer_fault_tests ${LIBS_INCLUDE_DIR}/tests/GuestOutputPointerFault.cpp) + target_include_directories(guest_output_pointer_fault_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_output_pointer_fault_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_output_pointer_fault_tests) + add_test(NAME guest_output_pointer_fault COMMAND guest_output_pointer_fault_tests) + set_tests_properties(guest_output_pointer_fault PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_output_pointer_fault PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fstat_fault_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatFault.cpp) target_include_directories(guest_fstat_fault_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fstat_fault_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index cc99d90231..26acca545b 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -363,7 +363,7 @@ extern "C" unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds) static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); -extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output) { +static int GetitimerInto(int which, GuestItimerval* output) { if (which < 0 || which > 2) { *__error_nid_postfix() = 22; return -1; @@ -394,6 +394,16 @@ extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output return 0; } +extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output) { + GuestItimerval result{}; + const int status = GetitimerInto(which, output == nullptr ? nullptr : &result); + if (status == 0 && !GuestCopyOut_nid_no_patch(output, &result, sizeof(result))) { + *__error_nid_postfix() = 14; + return -1; + } + return status; +} + namespace { constexpr int ItimerReal = 0; constexpr int ItimerProf = 2; @@ -421,7 +431,7 @@ extern "C" int APS5_VABI setitimer_nid_postfix(int which, const GuestItimerval* if (value == nullptr && previous == nullptr) { *__error_nid_postfix() = 14; return -1; } GuestItimerval requested{}; if (value != nullptr) { - requested = *value; + if (!GuestCopyIn_nid_no_patch(&requested, value, sizeof(requested))) { *__error_nid_postfix() = 14; return -1; } if (!ValidItimer(requested.value)) { *__error_nid_postfix() = 22; return -1; } if (!ItimerSet(requested.value)) requested.interval = {}; else if (!ValidItimer(requested.interval)) { *__error_nid_postfix() = 22; return -1; } @@ -429,7 +439,10 @@ extern "C" int APS5_VABI setitimer_nid_postfix(int which, const GuestItimerval* if (which != ItimerReal) { if (ItimerSet(requested.value)) throw std::runtime_error(std::string("setitimer: ") + (which == ItimerProf ? "ITIMER_PROF" : "ITIMER_VIRTUAL") + " needs process CPU time accounting, which is not implemented"); - if (previous != nullptr) *previous = {}; + if (previous != nullptr) { + const GuestItimerval cleared{}; + if (!GuestCopyOut_nid_no_patch(previous, &cleared, sizeof(cleared))) { *__error_nid_postfix() = 14; return -1; } + } return 0; } if (requested.interval.tv_sec == 0 && requested.interval.tv_usec != 0 && requested.interval.tv_usec < MinExactIntervalMicros) @@ -448,7 +461,7 @@ extern "C" int APS5_VABI setitimer_nid_postfix(int which, const GuestItimerval* NotifyAlarms(alarms); } } - if (previous != nullptr) *previous = old; + if (previous != nullptr && !GuestCopyOut_nid_no_patch(previous, &old, sizeof(old))) { *__error_nid_postfix() = 14; return -1; } return 0; } diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 4e3d97ebca..6decf5b8c8 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -501,9 +501,11 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { return 0; } -int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { - KernelTimeval ignoredTime{}; - if (tv == nullptr) tv = &ignoredTime; +int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* output, KernelTimezone* zoneOutput) { + KernelTimeval current{}; + auto* tv = ¤t; + KernelTimezone zone{}; + auto* tz = zoneOutput == nullptr ? nullptr : &zone; #ifdef _WIN32 FILETIME ft{}; GetSystemTimePreciseAsFileTime(&ft); @@ -523,6 +525,14 @@ int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { tz->tz_minuteswest = 0; tz->tz_dsttime = 0; } + if (output != nullptr && !GuestCopyOut_nid_no_patch(output, ¤t, sizeof(current))) { + *__error_nid_postfix() = 14; + return -1; + } + if (zoneOutput != nullptr && !GuestCopyOut_nid_no_patch(zoneOutput, &zone, sizeof(zone))) { + *__error_nid_postfix() = 14; + return -1; + } return 0; } @@ -705,13 +715,14 @@ int APS5_VABI sceKernelConvertUtcToLocaltime(int64_t utc_time, int64_t* local_ti int APS5_VABI sceKernelGettimeofday(KernelTimeval* tp) { if (tp == nullptr) return SceKernelErrorEfault; - return gettimeofday_nid_postfix(tp, nullptr); + const int result = gettimeofday_nid_postfix(tp, nullptr); + return result != 0 && *__error_nid_postfix() == 14 ? SceKernelErrorEfault : result; } int APS5_VABI sceKernelGettimezone(KernelTimezone* tz) { if (tz == nullptr) return SceKernelErrorEfault; - *tz = {0, 0}; - return 0; + const KernelTimezone zone{0, 0}; + return GuestCopyOut_nid_no_patch(tz, &zone, sizeof(zone)) ? 0 : SceKernelErrorEfault; } uint64_t APS5_VABI sceKernelReadTsc(void) { diff --git a/core/libs/tests/GuestOutputPointerFault.cpp b/core/libs/tests/GuestOutputPointerFault.cpp new file mode 100644 index 0000000000..5645542910 --- /dev/null +++ b/core/libs/tests/GuestOutputPointerFault.cpp @@ -0,0 +1,57 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/include/Api.hpp" + +#include +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "output pointer check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr int GuestEfault = 14; +static constexpr int KernelEfault = static_cast(0x8002000Eu); + +static bool Efault(int result) { + return result == -1 && *__error_nid_postfix() == GuestEfault; +} + +int main() { + alignas(8) unsigned char valid[512]{}; + alignas(8) unsigned char zeroTimer[32]{}; + auto* timeValid = reinterpret_cast(valid); + auto* zoneValid = reinterpret_cast(valid + 32); + auto* timerValid = reinterpret_cast(valid); + auto* zeroTimerValue = reinterpret_cast(zeroTimer); + for (const std::uintptr_t address : {std::uintptr_t{1}, ~std::uintptr_t{0xfff}}) { + void* bad = reinterpret_cast(address); + auto* badTime = reinterpret_cast(address); + auto* badZone = reinterpret_cast(address); + auto* badTimer = reinterpret_cast(address); + *__error_nid_postfix() = 0; + *__error_nid_postfix() = 0; + *__error_nid_postfix() = 0; + Require(Efault(gettimeofday_nid_postfix(badTime, nullptr))); + *__error_nid_postfix() = 0; + Require(Efault(gettimeofday_nid_postfix(timeValid, badZone))); + *__error_nid_postfix() = 0; + Require(Efault(getitimer_nid_postfix(0, badTimer))); + *__error_nid_postfix() = 0; + Require(Efault(setitimer_nid_postfix(0, zeroTimerValue, badTimer))); + *__error_nid_postfix() = 0; + Require(Efault(setitimer_nid_postfix(0, badTimer, timerValid))); + Require(sceKernelGettimeofday(badTime) == KernelEfault); + Require(sceKernelGettimezone(badZone) == KernelEfault); + } + Require(gettimeofday_nid_postfix(timeValid, zoneValid) == 0); + Require(getitimer_nid_postfix(0, timerValid) == 0); + Require(setitimer_nid_postfix(0, zeroTimerValue, timerValid) == 0); + Require(sceKernelGettimeofday(timeValid) == 0); + Require(sceKernelGettimezone(zoneValid) == 0); +} From 4ab16365fb0b1e9ca968ffda4ce529fd43f73c90 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:23:22 +0100 Subject: [PATCH 060/702] fix(libc): check the guest read on macOS and pin the setitimer order GuestCopyIn_nid_no_patch fell through to a plain memcpy on macOS, so an unreadable pointer was dereferenced in the host. Read with mach_vm_read_overwrite and require KERN_SUCCESS and a full copy, the mirror of the mach_vm_write branch in GuestCopyOut_nid_no_patch and of CopyGuestBytes. The test now also checks the order FreeBSD 11.4 uses in sys_setitimer (sys/kern/kern_time.c:803-808): the new timer is set first and the old value is copied out afterwards, so a failing copyout reports EFAULT and leaves the timer armed. The macOS branch has not been run. Co-Authored-By: Claude Sonnet 5.5 Co-authored-by: AdnanKhan-ak47 <93566182+AdnanKhan-ak47@users.noreply.github.com> --- core/libs/prx/libc/src/General.cpp | 4 ++++ core/libs/tests/GuestOutputPointerFault.cpp | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index e20870290f..e8b4e2ed78 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -862,6 +862,10 @@ extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, return true; } return static_cast(copied) == bytes; +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + return ::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(source), bytes, + reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; #else std::memcpy(destination, source, bytes); return true; diff --git a/core/libs/tests/GuestOutputPointerFault.cpp b/core/libs/tests/GuestOutputPointerFault.cpp index 5645542910..9e9ee4546e 100644 --- a/core/libs/tests/GuestOutputPointerFault.cpp +++ b/core/libs/tests/GuestOutputPointerFault.cpp @@ -52,6 +52,13 @@ int main() { Require(gettimeofday_nid_postfix(timeValid, zoneValid) == 0); Require(getitimer_nid_postfix(0, timerValid) == 0); Require(setitimer_nid_postfix(0, zeroTimerValue, timerValid) == 0); + GuestItimerval armed{}; + armed.value.tv_sec = 3600; + *__error_nid_postfix() = 0; + Require(Efault(setitimer_nid_postfix(0, &armed, reinterpret_cast(~std::uintptr_t{0xfff})))); + GuestItimerval current{}; + Require(getitimer_nid_postfix(0, ¤t) == 0 && current.value.tv_sec > 0); + Require(setitimer_nid_postfix(0, zeroTimerValue, nullptr) == 0); Require(sceKernelGettimeofday(timeValid) == 0); Require(sceKernelGettimezone(zoneValid) == 0); } From d69520c794feeaaddcd5cbd409dd738fb6ae4504 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:49:36 +0100 Subject: [PATCH 061/702] fix(libc): throw from GuestCopyIn on a Linux read failure other than EFAULT When process_vm_readv failed with anything but EFAULT (EPERM under a seccomp filter, ENOSYS), GuestCopyIn_nid_no_patch fell back to a plain memcpy, so the host dereferenced the unchecked guest pointer, the crash the checked read exists to prevent. Throw as GuestCopyOut_nid_no_patch does for process_vm_writev. The test runs the call in a child process under a seccomp filter that answers process_vm_readv with EPERM and expects the exception. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/General.cpp | 3 +- core/libs/tests/GuestOutputPointerFault.cpp | 41 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index e8b4e2ed78..ae36b25693 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -858,8 +858,7 @@ extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, const ssize_t copied = ::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0); if (copied < 0) { if (errno == EFAULT) return false; - std::memcpy(destination, source, bytes); - return true; + throw std::runtime_error("GuestCopyIn: process_vm_readv failed with errno " + std::to_string(errno)); } return static_cast(copied) == bytes; #elif defined(__APPLE__) diff --git a/core/libs/tests/GuestOutputPointerFault.cpp b/core/libs/tests/GuestOutputPointerFault.cpp index 9e9ee4546e..f299c106ec 100644 --- a/core/libs/tests/GuestOutputPointerFault.cpp +++ b/core/libs/tests/GuestOutputPointerFault.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/include/Api.hpp" @@ -6,6 +7,15 @@ #include #include #include +#include +#ifdef __linux__ +#include +#include +#include +#include +#include +#include +#endif static void Check(bool value, int line) { if (!value) { @@ -22,6 +32,34 @@ static bool Efault(int result) { return result == -1 && *__error_nid_postfix() == GuestEfault; } +#ifdef __linux__ +static void RequireNonEfaultFailureThrows() { + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + sock_filter filter[] = { + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_process_vm_readv, 0, 1), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + }; + sock_fprog program{static_cast(sizeof(filter) / sizeof(filter[0])), filter}; + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 || prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program) != 0) _exit(2); + char source[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + char destination[8] = {}; + try { + GuestCopyIn_nid_no_patch(destination, source, sizeof(source)); + } catch (const std::runtime_error&) { + _exit(destination[0] == 0 ? 0 : 3); + } + _exit(1); + } + int status = 0; + Require(waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} +#endif + int main() { alignas(8) unsigned char valid[512]{}; alignas(8) unsigned char zeroTimer[32]{}; @@ -61,4 +99,7 @@ int main() { Require(setitimer_nid_postfix(0, zeroTimerValue, nullptr) == 0); Require(sceKernelGettimeofday(timeValid) == 0); Require(sceKernelGettimezone(zoneValid) == 0); +#ifdef __linux__ + RequireNonEfaultFailureThrows(); +#endif } From e632137d7cd837ff203719fb7581669a5c405ed4 Mon Sep 17 00:00:00 2001 From: dwk001 Date: Sun, 25 Oct 2026 12:53:57 +0000 Subject: [PATCH 062/702] fix(libkernel): keep the mutex kind as a signed 16-bit value as firmware 13.42 does Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B7MJ4KkM4JNNWQxjsXT4ib --- .../prx/libkernel/Pthread/Posix/Mutex.cpp | 4 ++-- core/libs/tests/GuestPthreadMutexattr.cpp | 20 ++++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index d0eadf7003..8d9a0f7ebf 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -166,7 +166,7 @@ int APS5_VABI pthread_mutexattr_settype_nid_postfix(PthreadMutexattr* attr, int int APS5_VABI pthread_mutexattr_gettype_nid_postfix(const PthreadMutexattr* attr, int* type) { if (!attr || !*attr) return POSIX_EINVAL; if (!type) throw std::runtime_error("pthread_mutexattr_gettype: null type"); - if (static_cast((*attr)->type) > static_cast(MutexType::Adaptive)) return POSIX_EINVAL; + if (static_cast((*attr)->type) > static_cast(MutexType::Adaptive)) return POSIX_EINVAL; *type = static_cast((*attr)->type); return 0; } @@ -176,7 +176,7 @@ int APS5_VABI pthread_mutexattr_setkind_np_nid_postfix(PthreadMutexattr* attr, i *__error_nid_postfix() = POSIX_EINVAL; return -1; } - (*attr)->type = static_cast(kind); + (*attr)->type = static_cast(static_cast(static_cast(kind))); return 0; } diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 73e1e28a97..994248f2f9 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -120,9 +120,27 @@ static void CheckKind() { Require(pthread_mutexattr_setkind_np_nid_postfix(&attr, invalid) == 0); Require(pthread_mutexattr_getkind_np_nid_postfix(attr) == invalid); type = -1; - if (invalid != 0) Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == GUEST_EINVAL && type == -1); + if (invalid == 5) Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == GUEST_EINVAL && type == -1); + if (invalid != 5) Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == invalid); Require(pthread_mutex_init_nid_postfix(&mutex, &attr) == GUEST_EINVAL && mutex == nullptr); } + for (const int negative : {-2, -32768}) { + Require(pthread_mutexattr_setkind_np_nid_postfix(&attr, negative) == 0); + type = 0; + Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == negative); + } + Require(pthread_mutexattr_setkind_np_nid_postfix(&attr, 0x8000) == 0); + Require(pthread_mutexattr_getkind_np_nid_postfix(attr) == -32768); + for (const int large : {32767, 0x12345}) { + Require(pthread_mutexattr_setkind_np_nid_postfix(&attr, large) == 0); + type = -1; + Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == GUEST_EINVAL && type == -1); + } + Require(pthread_mutexattr_getkind_np_nid_postfix(attr) == 0x2345); + Require(pthread_mutexattr_setkind_np_nid_postfix(&attr, 0x10001) == 0); + Require(pthread_mutexattr_getkind_np_nid_postfix(attr) == 1); + Require(pthread_mutex_init_nid_postfix(&mutex, &attr) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); Require(pthread_mutexattr_setkind_np_nid_postfix(&attr, 2) == 0); Require(pthread_mutex_init_nid_postfix(&mutex, &attr) == 0); Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 96d22d315c..785df67167 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -264,7 +264,7 @@ - [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same - [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports the host volume of a file, not a console mount: block sizes, block counts, name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0). The type, mount names, owner, filesystem id and I/O counters are 0 or empty. Sockets, pipes and kqueues return `EINVAL`, as FreeBSD's `getvnode` does. A character device, including `/dev/random` and `/dev/urandom`, reports FreeBSD 11.4's [`devfs_statfs`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/fs/devfs/devfs_vfsops.c#L225-L237) values on every host: no flags, 512-byte blocks, 2 blocks, nothing free and a 255-byte name limit. A Windows descriptor that is none of these throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever -- [pthread_mutexattr_setkind_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) stores any kind, as FreeBSD 11.4's [`_pthread_mutexattr_setkind_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutexattr.c#L108-L120) does; `pthread_mutexattr_gettype` then fails with `EINVAL` for a kind of 5 or more (negative kinds included, as the unsigned type), and `pthread_mutex_init` and `scePthreadMutexInit` fail with `EINVAL` for a kind outside 1 to 4, as [`mutex_check_attr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L207-L218) does. Not measured on a PS5 +- [pthread_mutexattr_setkind_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) keeps the low 16 bits of the kind as a signed value; `pthread_mutexattr_gettype` fails with `EINVAL` only for a kind above 4, so a negative kind is returned with success, and `pthread_mutex_init` and `scePthreadMutexInit` fail with `EINVAL` for a kind outside 1 to 4 without touching the mutex. Measured in #10725 by running the firmware 13.42 `libkernel.sprx` and `libkernel_sys.sprx` in an emulator, not on a console or in a title process - [pthread_mutex_getspinloops_np, pthread_mutex_setspinloops_np, pthread_mutex_getyieldloops_np, pthread_mutex_setyieldloops_np and their scePthreadMutex counterparts](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) store and report the counts, but host mutexes do not spin or yield by them. The defaults follow FreeBSD 11.4's [`mutex_init_body`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L236-L272) without `LIBPTHREAD_SPINLOOPS` and `LIBPTHREAD_YIELDLOOPS`: 2000 spins for an adaptive mutex, 0 otherwise; the console's defaults are unverified. Setting the yield count of a destroyed mutex returns 0 as FreeBSD's [`__pthread_mutex_setyieldloops_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L1125-L1135) does; the console's result is unverified - [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 - [pthread_cond_signalto_np, scePthreadCondSignalto](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) wake every waiter of the condition variable, not only the given thread, and do not check the thread; the other waiters see a spurious wakeup. Which waiters the console wakes, and its result for a thread that is not waiting, are unverified From 4c8e1d194e2397c4302d5a3fdade33c008fa8eda Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 00:50:01 +0400 Subject: [PATCH 063/702] fix(tools): merge only the pull requests that share files or exports with the branch in pr_overlap pr_overlap --branch merged every open pull request against main, although a pull request can only appear in the report when it shares a file or an export with the branch, so on a repository with a thousand open pull requests the check did not finish in an hour. probe lists each pull request's files and exports from git diff base...head, without a merge, and scan_all merges only the ones that overlap the branch; the others get empty sets, which is what they would contribute to the report. --out is unchanged. --- tools/pr_overlap.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index eadcf79576..1634affa97 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -241,9 +241,31 @@ def scan(pr): pr["removed_exports"] = source_changed_exports(diff, "-") +def probe(pr): + changes = git("diff", "--name-only", "--no-renames", "-z", f"refs/pr/base...{pr['ref']}").split("\0") + files = {path for path in changes if path} + if not any(path.startswith("core/libs/prx/") and path.endswith(".cpp") for path in files): + return files, set() + return files, exports(git("diff", "-U0", "--no-color", f"refs/pr/base...{pr['ref']}", "--", "core/libs/prx/*.cpp")) + + +def related(prs, executor): + mine = prs[0] + scan(mine) + others = {n: pr for n, pr in prs.items() if n} + chosen = {} + for (n, pr), (files, found) in zip(others.items(), executor.map(probe, others.values())): + if files & mine["files"] or found & mine["exports"]: + chosen[n] = pr + else: + pr.update(tree=None, files=set(), added=set(), exports=set()) + return chosen + + def scan_all(prs, jobs): with ThreadPoolExecutor(max_workers=jobs) as executor: - list(executor.map(scan, prs.values())) + pending = related(prs, executor) if 0 in prs else prs + list(executor.map(scan, pending.values())) def hunks(pr, path): From 4ebd235d450319b9f55dd4e1f6b752ab0f1a94a4 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 00:50:01 +0400 Subject: [PATCH 064/702] test(tools): cover the pr_overlap --branch prefilter The branch scan gives the same overlaps as a scan of every pull request and merges only the pull requests that share a file or an export with the branch; a scan without a branch still merges all of them. --- tools/tests/test_pr_overlap_branch_scan.py | 107 +++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tools/tests/test_pr_overlap_branch_scan.py diff --git a/tools/tests/test_pr_overlap_branch_scan.py b/tools/tests/test_pr_overlap_branch_scan.py new file mode 100644 index 0000000000..f02ea7ac9a --- /dev/null +++ b/tools/tests/test_pr_overlap_branch_scan.py @@ -0,0 +1,107 @@ +import copy +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + +EXPORT = "int APS5_VABI shared_nid_postfix(int value) { return value; }\n" + + +def run(*args): + return subprocess.run(["git", "-c", "user.name=t", "-c", "user.email=t@t", *args], + capture_output=True, text=True, check=True).stdout.strip() + + +def write(path, text): + Path(path).parent.mkdir(parents=True, exist_ok=True) + Path(path).write_text(text, encoding="utf-8", newline="\n") + + +def commit_on_base(base, message, files): + run("checkout", "-q", "--detach", base) + for path, text in files.items(): + write(path, text) + run("add", "-A") + run("commit", "-q", "-m", message) + return run("rev-parse", "HEAD") + + +class BranchScanTests(unittest.TestCase): + def setUp(self): + self.old = os.getcwd() + self.tmp = tempfile.TemporaryDirectory() + os.chdir(self.tmp.name) + run("init", "-q") + for name in ("a.txt", "b.txt", "c.txt"): + write(name, "one\ntwo\nthree\n") + write("core/libs/prx/lib/Existing.cpp", "int Existing() { return 0; }\n") + run("add", "-A") + run("commit", "-q", "-m", "base") + base = run("rev-parse", "HEAD") + run("update-ref", "refs/pr/base", base) + branch = commit_on_base(base, "branch", {"a.txt": "branch\ntwo\nthree\n", "core/libs/prx/lib/Branch.cpp": EXPORT}) + numbers = { + 1: commit_on_base(base, "same lines", {"a.txt": "other\ntwo\nthree\n"}), + 2: commit_on_base(base, "unrelated file", {"c.txt": "one\ntwo\nchanged\n"}), + 3: commit_on_base(base, "same export elsewhere", {"core/libs/prx/lib/Other.cpp": EXPORT}), + 4: commit_on_base(base, "unrelated export", {"core/libs/prx/lib/Third.cpp": "int APS5_VABI other_nid_postfix(int value) { return value; }\n"}), + } + for number, sha in numbers.items(): + run("update-ref", f"refs/pr/{number}", sha) + self.prs = {n: {"ref": f"refs/pr/{n}", "sha": sha, "author": f"author{n}", "depends": set()} for n, sha in numbers.items()} + self.prs[0] = {"ref": branch, "author": None, "depends": set()} + + def tearDown(self): + os.chdir(self.old) + self.tmp.cleanup() + + def test_reports_match_a_scan_of_every_pull_request(self): + everything = copy.deepcopy(self.prs) + for pr in everything.values(): + pr_overlap.scan(pr) + pr_overlap.scan_all(self.prs, 2) + + for number in (1, 2, 3, 4): + with self.subTest(number=number): + self.assertEqual(pr_overlap.overlaps(self.prs, 0, number), pr_overlap.overlaps(everything, 0, number)) + self.assertIn("a.txt", pr_overlap.overlaps(self.prs, 0, 1)["conflicts"]) + self.assertEqual(pr_overlap.overlaps(self.prs, 0, 3)["exports"], {"shared"}) + self.assertIsNone(pr_overlap.overlaps(self.prs, 0, 2)) + self.assertIsNone(pr_overlap.overlaps(self.prs, 0, 4)) + + def test_only_pull_requests_sharing_a_file_or_an_export_are_merged(self): + merged = [] + + def record(*args): + merged.append(args[1]) + return real(*args) + + real = pr_overlap.merge + with patch.object(pr_overlap, "merge", side_effect=record): + pr_overlap.scan_all(self.prs, 2) + + self.assertCountEqual(merged, [self.prs[0]["ref"], "refs/pr/1", "refs/pr/3"]) + + def test_scanning_without_a_branch_still_merges_every_pull_request(self): + merged = [] + + def record(*args): + merged.append(args[1]) + return real(*args) + + real = pr_overlap.merge + del self.prs[0] + with patch.object(pr_overlap, "merge", side_effect=record): + pr_overlap.scan_all(self.prs, 2) + + self.assertCountEqual(merged, ["refs/pr/1", "refs/pr/2", "refs/pr/3", "refs/pr/4"]) + + +if __name__ == "__main__": + unittest.main() From a9f1ade5d6d42e73f8af34c6350fd39a4e1e04d4 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 05:28:16 +0400 Subject: [PATCH 065/702] test(tools): register the pr_overlap branch scan test test_script_registration fails for a test script that no add_test or workflow runs, which stopped the relinker matrix on the previous commit. --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 813ad77f6e..40d4efb68c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -225,6 +225,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py) add_test(NAME pr_overlap_errors COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py) + add_test(NAME pr_overlap_branch_scan COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_branch_scan.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} From 1456c76806fb77d97f71719521057ff9015086d5 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 13:03:37 +0400 Subject: [PATCH 066/702] test(tools): write the branch scan fixtures without Path.write_text(newline=) Path.write_text takes newline only from Python 3.10, so the three tests errored in the fixture helper on the Python 3.9.6 that macOS ships as /usr/bin/python3. open() with newline writes the same LF bytes on every version. --- tools/tests/test_pr_overlap_branch_scan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_pr_overlap_branch_scan.py b/tools/tests/test_pr_overlap_branch_scan.py index f02ea7ac9a..7ac2e8baed 100644 --- a/tools/tests/test_pr_overlap_branch_scan.py +++ b/tools/tests/test_pr_overlap_branch_scan.py @@ -20,7 +20,8 @@ def run(*args): def write(path, text): Path(path).parent.mkdir(parents=True, exist_ok=True) - Path(path).write_text(text, encoding="utf-8", newline="\n") + with open(path, "w", encoding="utf-8", newline="\n") as handle: + handle.write(text) def commit_on_base(base, message, files): From a2f9601332b33fcd72dc78495c85ec01a91e4b8a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:23:47 +0000 Subject: [PATCH 067/702] feat(tools): report pr_overlap's pull request count and fetch progress pr_overlap printed nothing until its report, which takes 54 to 81 s on Linux with about 1,300 open pull requests and took minutes on Windows, so a run looked stuck. Write progress lines to stderr: the number of open pull requests once they are listed, and the number of pull request heads fetched after each batch of 100. The report on stdout is unchanged. --- tools/pr_overlap.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 1634affa97..ea4a83d7ab 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -21,6 +21,7 @@ PLAIN_DEPENDS = re.compile(r'Depends on[ \t]+(#\d+\b(?:(?:[ \t]*,[ \t]*(?:and[ \ HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))?", re.M) MARKER = "" FETCH_REFS = 100 +PROGRESS_EVERY = 100 _REPO_ROOTS = {} @@ -78,6 +79,10 @@ def depends_on(body): match = next((match for match in DEPENDS.finditer(body) if match.group(1).strip()), None) or PLAIN_DEPENDS.search(body) return {int(n) for n in re.findall(r"#(\d+)", match.group(1))} if match else set() +def progress(message): + print(f"pr_overlap: {message}", file=sys.stderr, flush=True) + + def open_prs(base): query = urlencode({"state": "open", "base": base, "per_page": 100}) out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") @@ -106,6 +111,7 @@ def open_prs(base): "author": pr["user"]["login"], "depends": depends_on(pr["body"]), } + progress(f"{len(prs)} open pull requests against {base}") return prs @@ -113,6 +119,7 @@ def fetch(base, prs): refspecs = [f"+refs/heads/{base}:refs/pr/base"] + [f"+{pr['sha']}:{pr['ref']}" for pr in prs.values()] for start in range(0, len(refspecs), FETCH_REFS): git("fetch", "--no-tags", "-q", f"https://github.com/{REPO}.git", *refspecs[start:start + FETCH_REFS]) + progress(f"fetched {min(start + FETCH_REFS, len(refspecs)) - 1}/{len(prs)} pull request heads") def merge(*args): From ddd04de0c90ff207565ab2ecaeda29762b6e49ac Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:24:26 +0000 Subject: [PATCH 068/702] test(tools): cover pr_overlap's pull request count and fetch progress Check that listing 150 pull requests over two pages reports their number, that fetching 250 heads in three batches reports 99, 199 and 250 of them, and that a run without pull requests still reports one fetch line. The tests fail before the previous commit. --- tools/tests/test_pr_overlap_progress.py | 51 +++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tools/tests/test_pr_overlap_progress.py diff --git a/tools/tests/test_pr_overlap_progress.py b/tools/tests/test_pr_overlap_progress.py new file mode 100644 index 0000000000..cc9d834365 --- /dev/null +++ b/tools/tests/test_pr_overlap_progress.py @@ -0,0 +1,51 @@ +import contextlib +import io +import json +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + + +def pull(number): + return {"number": number, "head": {"sha": f"{number:040x}"}, "user": {"login": "someone"}, "body": ""} + + +class ProgressTests(unittest.TestCase): + def capture(self, function, *args): + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + result = function(*args) + return result, stderr.getvalue().splitlines() + + def test_listing_reports_the_number_of_open_pull_requests(self): + pages = json.dumps([pull(n) for n in range(1, 101)]) + "\n" + json.dumps([pull(n) for n in range(101, 151)]) + with patch.object(pr_overlap, "gh", return_value=pages), patch.object(pr_overlap, "REPO", "owner/project", create=True): + prs, lines = self.capture(pr_overlap.open_prs, "main") + self.assertEqual(len(prs), 150) + self.assertEqual(lines, ["pr_overlap: 150 open pull requests against main"]) + + def test_fetch_reports_the_heads_fetched_after_each_batch(self): + prs = {n: {"ref": f"refs/pr/{n}", "sha": f"{n:040x}"} for n in range(1, 251)} + calls = [] + with patch.object(pr_overlap, "git", side_effect=lambda *args: calls.append(args) or ""), \ + patch.object(pr_overlap, "REPO", "owner/project", create=True): + _, lines = self.capture(pr_overlap.fetch, "main", prs) + self.assertEqual(len(calls), 3) + self.assertEqual(lines, [ + "pr_overlap: fetched 99/250 pull request heads", + "pr_overlap: fetched 199/250 pull request heads", + "pr_overlap: fetched 250/250 pull request heads", + ]) + + def test_fetch_without_pull_requests_still_reports_once(self): + with patch.object(pr_overlap, "git", return_value=""), patch.object(pr_overlap, "REPO", "owner/project", create=True): + _, lines = self.capture(pr_overlap.fetch, "main", {}) + self.assertEqual(lines, ["pr_overlap: fetched 0/0 pull request heads"]) + + +if __name__ == "__main__": + unittest.main() From 17aac28ca1b5df39e42f2dadf8fc29daf6c9c37d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:24:35 +0000 Subject: [PATCH 069/702] build: register the pr_overlap progress test Run tools/tests/test_pr_overlap_progress.py as pr_overlap_progress next to pr_overlap_branch_scan, so check_test_registration --scripts finds a runner for it. --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 40d4efb68c..989d0a06bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -227,6 +227,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py) add_test(NAME pr_overlap_branch_scan COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_branch_scan.py) + add_test(NAME pr_overlap_progress COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_progress.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} From 56cff28e40f50eae19e00251fa9e095c04ff7d40 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:24:53 +0000 Subject: [PATCH 070/702] feat(tools): report pr_overlap's probe and merge progress After the fetch, a --branch run probed about 1,300 pull requests for about 28 s without output, and --out merged every pull request into the base. Report every 100 pull requests and at the end how many were probed and how many of them share files or exports with the branch, and how many were merged into the base. --- tools/pr_overlap.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index ea4a83d7ab..6ef02a43b7 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -261,18 +261,22 @@ def related(prs, executor): scan(mine) others = {n: pr for n, pr in prs.items() if n} chosen = {} - for (n, pr), (files, found) in zip(others.items(), executor.map(probe, others.values())): + for index, ((n, pr), (files, found)) in enumerate(zip(others.items(), executor.map(probe, others.values())), 1): if files & mine["files"] or found & mine["exports"]: chosen[n] = pr else: pr.update(tree=None, files=set(), added=set(), exports=set()) + if index % PROGRESS_EVERY == 0 or index == len(others): + progress(f"probed {index}/{len(others)} pull requests, {len(chosen)} share files or exports with the branch") return chosen def scan_all(prs, jobs): with ThreadPoolExecutor(max_workers=jobs) as executor: pending = related(prs, executor) if 0 in prs else prs - list(executor.map(scan, pending.values())) + for index, _ in enumerate(executor.map(scan, pending.values()), 1): + if index % PROGRESS_EVERY == 0 or index == len(pending): + progress(f"merged {index}/{len(pending)} pull requests into the base") def hunks(pr, path): From 4480d9d42795df604e143f982b7a7a67c1e0f8a1 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:25:17 +0000 Subject: [PATCH 071/702] test(tools): cover pr_overlap's probe and merge progress With 250 pull requests of which every 25th shares a file with the branch, expect probe lines at 100, 200 and 250 with 4, 8 and 10 related pull requests and one line for merging those 10; without a branch, expect merge lines at 100 and 150 of 150. The tests fail before the previous commit. --- tools/tests/test_pr_overlap_progress.py | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tools/tests/test_pr_overlap_progress.py b/tools/tests/test_pr_overlap_progress.py index cc9d834365..f1d263668a 100644 --- a/tools/tests/test_pr_overlap_progress.py +++ b/tools/tests/test_pr_overlap_progress.py @@ -47,5 +47,34 @@ class ProgressTests(unittest.TestCase): self.assertEqual(lines, ["pr_overlap: fetched 0/0 pull request heads"]) + def fake_scan(self, pr): + pr.update(files=set(pr.get("files_on_scan", ())), added=set(), exports=set(), tree="tree") + + def test_branch_run_reports_probing_and_merging(self): + prs = {0: {"ref": "branch", "files_on_scan": {"shared.txt"}}} + prs.update({n: {"ref": f"refs/pr/{n}"} for n in range(1, 251)}) + + def fake_probe(pr): + number = int(pr["ref"].rsplit("/", 1)[1]) + return ({"shared.txt"} if number % 25 == 0 else {"other.txt"}), set() + + with patch.object(pr_overlap, "scan", side_effect=self.fake_scan), patch.object(pr_overlap, "probe", side_effect=fake_probe): + _, lines = self.capture(pr_overlap.scan_all, prs, 2) + self.assertEqual(lines, [ + "pr_overlap: probed 100/250 pull requests, 4 share files or exports with the branch", + "pr_overlap: probed 200/250 pull requests, 8 share files or exports with the branch", + "pr_overlap: probed 250/250 pull requests, 10 share files or exports with the branch", + "pr_overlap: merged 10/10 pull requests into the base", + ]) + + def test_full_run_reports_every_hundred_merges(self): + prs = {n: {"ref": f"refs/pr/{n}"} for n in range(1, 151)} + with patch.object(pr_overlap, "scan", side_effect=self.fake_scan): + _, lines = self.capture(pr_overlap.scan_all, prs, 2) + self.assertEqual(lines, [ + "pr_overlap: merged 100/150 pull requests into the base", + "pr_overlap: merged 150/150 pull requests into the base", + ]) + if __name__ == "__main__": unittest.main() From b5424e9bde188c23ec412aee4d5736c59e77b266 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:51:20 +0000 Subject: [PATCH 072/702] perf(tools): fetch pr_overlap's pull request pages in parallel --- tools/pr_overlap.py | 51 +++++++++++++++++++++++++--------- tools/tests/test_pr_overlap.py | 4 +-- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 6ef02a43b7..920c83f1d6 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -83,27 +83,52 @@ def progress(message): print(f"pr_overlap: {message}", file=sys.stderr, flush=True) -def open_prs(base): - query = urlencode({"state": "open", "base": base, "per_page": 100}) - out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") - prs = {} +LAST_PAGE = re.compile(r'[?&]page=(\d+)>; rel="last"') + + +def page(url): + out = gh("api", "--include", url) + if not out.startswith("HTTP/"): + return "", out + blank = re.search(r"\r?\n\r?\n", out) + return (out, "") if blank is None else (out[:blank.start()], out[blank.end():]) + + +def json_pages(out): decoder = json.JSONDecoder() offset = 0 - page_count = 0 - pull_requests = [] + pages = [] while offset < len(out): while offset < len(out) and out[offset].isspace(): offset += 1 if offset == len(out): break - page, offset = decoder.raw_decode(out, offset) - if not isinstance(page, list): - raise ValueError("gh api --paginate returned a non-array page") - page_count += 1 - pull_requests.extend(page) - if page_count == 0: - raise ValueError("gh api --paginate returned no JSON pages") + found, offset = decoder.raw_decode(out, offset) + if not isinstance(found, list): + raise ValueError("gh api returned a non-array page") + pages.append(found) + return pages + +def open_prs(base): + query = urlencode({"state": "open", "base": base, "per_page": 100}) + url = f"repos/{REPO}/pulls?{query}" + head, out = page(f"{url}&page=1") + pages = json_pages(out) + if not pages: + raise ValueError("gh api returned no JSON pages") + last = LAST_PAGE.search(head) + if last: + numbers = range(2, int(last.group(1)) + 1) + with ThreadPoolExecutor(max_workers=8) as executor: + for out in executor.map(lambda number: page(f"{url}&page={number}")[1], numbers): + pages.extend(json_pages(out)) + pushed_past_last = numbers.stop + while len(pages[-1]) == 100: + pages.extend(json_pages(page(f"{url}&page={pushed_past_last}")[1]) or [[]]) + pushed_past_last += 1 + pull_requests = [pr for found in pages for pr in found] + prs = {} for pr in pull_requests: prs[pr["number"]] = { "ref": f"refs/pr/{pr['number']}", diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index 4017ab3718..9daf91c70b 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -46,8 +46,8 @@ class OpenPullRequestsTests(unittest.TestCase): 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", - "repos/owner/project/pulls?state=open&base=topic%2Fone%26two&per_page=100", + "api", "--include", + "repos/owner/project/pulls?state=open&base=topic%2Fone%26two&per_page=100&page=1", ) def test_single_page_preserves_author_head_and_dependencies(self): From ab7a06e66c0907ad47d206036c2c42cab90c05bc Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:51:31 +0000 Subject: [PATCH 073/702] test(tools): cover pr_overlap's parallel pull request pages --- tools/tests/test_pr_overlap_pages.py | 94 ++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tools/tests/test_pr_overlap_pages.py diff --git a/tools/tests/test_pr_overlap_pages.py b/tools/tests/test_pr_overlap_pages.py new file mode 100644 index 0000000000..ea9d8543f7 --- /dev/null +++ b/tools/tests/test_pr_overlap_pages.py @@ -0,0 +1,94 @@ +import json +import sys +import threading +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + +URL = "repos/owner/project/pulls?state=open&base=main&per_page=100" + + +def pull(number): + return {"number": number, "user": {"login": f"author{number}"}, "head": {"sha": f"sha{number}"}, "body": None} + + +def response(numbers, last=None, newline="\r\n"): + headers = ["HTTP/2.0 200 OK", "Content-Type: application/json; charset=utf-8"] + if last: + headers.append(f'Link: ; rel="next", ' + f'; rel="last"') + return newline.join(headers) + newline + newline + json.dumps([pull(n) for n in numbers]) + + +class ServedPages: + def __init__(self, pages, last): + self.pages = pages + self.last = last + self.requested = [] + self.lock = threading.Lock() + + def __call__(self, *args): + self.assertion(args) + number = int(args[2].rsplit("&page=", 1)[1]) + with self.lock: + self.requested.append(number) + return response(self.pages.get(number, []), self.last if number == 1 else None) + + @staticmethod + def assertion(args): + if args[:2] != ("api", "--include") or not args[2].startswith(URL + "&page="): + raise AssertionError(f"unexpected gh call {args}") + + +class ParallelPageTests(unittest.TestCase): + def open_prs(self, served): + with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ + patch.object(pr_overlap, "gh", side_effect=served), \ + patch.object(pr_overlap, "progress"): + return pr_overlap.open_prs("main") + + def test_every_page_named_by_the_last_link_is_fetched(self): + pages = {1: range(1, 101), 2: range(101, 201), 3: range(201, 251)} + served = ServedPages(pages, last=3) + found = self.open_prs(served) + self.assertEqual(set(found), set(range(1, 251))) + self.assertCountEqual(served.requested, [1, 2, 3]) + self.assertEqual(found[250], {"ref": "refs/pr/250", "sha": "sha250", "author": "author250", "depends": set()}) + + def test_a_single_page_needs_one_request(self): + served = ServedPages({1: range(1, 43)}, last=None) + self.assertEqual(set(self.open_prs(served)), set(range(1, 43))) + self.assertEqual(served.requested, [1]) + + def test_a_full_last_page_is_followed_by_the_next_one(self): + pages = {1: range(1, 101), 2: range(101, 201), 3: range(201, 202)} + served = ServedPages(pages, last=2) + self.assertEqual(set(self.open_prs(served)), set(range(1, 202))) + self.assertEqual(sorted(served.requested), [1, 2, 3]) + + def test_a_full_last_page_followed_by_an_empty_one_stops(self): + served = ServedPages({1: range(1, 101), 2: range(101, 201)}, last=2) + self.assertEqual(set(self.open_prs(served)), set(range(1, 201))) + self.assertEqual(sorted(served.requested), [1, 2, 3]) + + def test_a_pull_request_shifted_onto_two_pages_is_kept_once(self): + served = ServedPages({1: range(1, 101), 2: range(100, 150)}, last=2) + found = self.open_prs(served) + self.assertEqual(len(found), 149) + + def test_headers_with_plain_newlines_are_split_from_the_body(self): + with patch.object(pr_overlap, "gh", return_value=response(range(1, 3), newline="\n")): + head, body = pr_overlap.page(URL + "&page=1") + self.assertTrue(head.startswith("HTTP/2.0 200 OK")) + self.assertEqual(body, json.dumps([pull(1), pull(2)])) + + def test_a_non_array_page_is_rejected(self): + with self.assertRaisesRegex(ValueError, "non-array page"): + pr_overlap.json_pages('{"message": "Not Found"}') + + +if __name__ == "__main__": + unittest.main() From 400f728794895c79c191374806535a3724d11e8a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:51:39 +0000 Subject: [PATCH 074/702] build: register the pr_overlap pages test --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 989d0a06bb..40351cb867 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -229,6 +229,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_branch_scan.py) add_test(NAME pr_overlap_progress COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_progress.py) + add_test(NAME pr_overlap_pages COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_pages.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} From b8ab4b1157207f728c29f71acfefac17623f4ddf Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:56:52 +0000 Subject: [PATCH 075/702] perf(tools): cache pr_overlap's base exports by source blob --- tools/pr_overlap.py | 86 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 920c83f1d6..983e5ab363 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -1,5 +1,5 @@ import argparse -import io +import hashlib import json import os import re @@ -8,7 +8,6 @@ import sys from concurrent.futures import ThreadPoolExecutor from itertools import combinations from pathlib import Path -import tarfile from urllib.parse import quote, urlencode import nid_names @@ -238,22 +237,79 @@ def source_changed_exports(diff, prefix): return found +def export_cache_version(): + parsers = (Path(__file__), Path(nid_names.__file__)) + return hashlib.sha256(b"".join(path.read_bytes() for path in parsers)).hexdigest() + + +def load_export_cache(path, version): + try: + cache = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError): + return {} + if not isinstance(cache, dict) or cache.get("version") != version or not isinstance(cache.get("files"), dict): + return {} + return cache["files"] + + +def save_export_cache(path, version, files): + temporary = path.with_name(f"{path.name}.{os.getpid()}.tmp") + try: + temporary.write_text(json.dumps({"version": version, "files": files}), encoding="utf-8") + os.replace(temporary, path) + except OSError: + temporary.unlink(missing_ok=True) + + +def base_sources(root, ref): + listing = subprocess.run(["git", "-C", root, "ls-tree", "-r", "-z", ref, "--", "core/libs"], + capture_output=True, check=True).stdout.decode("utf-8", errors="replace") + sources = [] + for entry in filter(None, listing.split("\0")): + info, name = entry.split("\t", 1) + mode, kind, blob = info.split() + path = Path(name) + if kind != "blob" or mode == "120000" or path.suffix != ".cpp" or any( + part.lower() in {"test", "tests"} for part in path.parts[2:-1]): + continue + sources.append((blob, name)) + return sources + + +def blob_texts(root, blobs): + if not blobs: + return {} + out = subprocess.run(["git", "-C", root, "cat-file", "--batch"], input="".join(f"{blob}\n" for blob in blobs).encode(), + capture_output=True, check=True).stdout + texts, offset = {}, 0 + for blob in blobs: + header_end = out.index(b"\n", offset) + size = int(out[offset:header_end].split()[2]) + texts[blob] = out[header_end + 1:header_end + 1 + size].decode("utf-8", errors="replace") + offset = header_end + 2 + size + return texts + + def base_exports(ref): root = git("rev-parse", "--show-toplevel").strip() - archive = subprocess.run( - ["git", "-C", root, "archive", "--format=tar", ref, "core/libs"], capture_output=True, check=True - ).stdout + cache_path = Path(root, git("rev-parse", "--git-common-dir").strip()) / "pr_overlap-exports.json" + version = export_cache_version() + cached = load_export_cache(cache_path, version) + sources = base_sources(root, ref) + keys = {(blob, name): f"{blob} {library_for_path(name)}" for blob, name in sources} + texts = blob_texts(root, sorted({blob for (blob, name), key in keys.items() if key not in cached})) + files = {} found = {} - with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as source: - for entry in source: - path = Path(entry.name) - if not entry.isfile() or path.suffix != ".cpp" or any( - part.lower() in {"test", "tests"} for part in path.parts[2:-1]): - continue - text = source.extractfile(entry).read().decode("utf-8", errors="replace") - library = library_for_path(entry.name) - for key, names in source_exports(text, library).items(): - found.setdefault(key, set()).update(names) + for (blob, name), key in keys.items(): + if key not in files: + files[key] = cached[key] if key in cached else [ + [library, nid, sorted(names)] for (library, nid), names in source_exports(texts[blob], library_for_path(name)).items()] + for library, nid, names in files[key]: + found.setdefault((library, nid), set()).update(names) + older_bases_fit = len(cached) + len(files) <= 4 * len(files) + kept = {**cached, **files} if older_bases_fit else files + if kept != cached: + save_export_cache(cache_path, version, kept) return found From ee9ce6ee772a95287a99e0467353996c6b650546 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:57:38 +0000 Subject: [PATCH 076/702] test(tools): cover pr_overlap's base export cache --- tools/tests/test_pr_overlap_export_cache.py | 113 ++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 tools/tests/test_pr_overlap_export_cache.py diff --git a/tools/tests/test_pr_overlap_export_cache.py b/tools/tests/test_pr_overlap_export_cache.py new file mode 100644 index 0000000000..c66efe5232 --- /dev/null +++ b/tools/tests/test_pr_overlap_export_cache.py @@ -0,0 +1,113 @@ +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + + +def run(*args): + return subprocess.run(["git", "-c", "user.name=t", "-c", "user.email=t@t", *args], + capture_output=True, text=True, check=True).stdout.strip() + + +def write(path, text): + Path(path).parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8", newline="\n") as handle: + handle.write(text) + + +def export(name): + return f"int APS5_VABI {name}(int value) {{ return value; }}\n" + + +class ExportCacheTests(unittest.TestCase): + def setUp(self): + self.old = os.getcwd() + self.tmp = tempfile.TemporaryDirectory() + os.chdir(self.tmp.name) + run("init", "-q") + write("core/libs/prx/libOne/One.cpp", export("sceOneFirst")) + write("core/libs/prx/libTwo/Two.cpp", export("sceTwoFirst")) + write("core/libs/prx/libTwo/Copy.cpp", export("sceTwoFirst")) + write("core/libs/prx/libTwo/tests/Test.cpp", export("sceTestOnly")) + write("core/libs/prx/libTwo/Header.hpp", export("sceHeaderOnly")) + run("add", "-A") + run("commit", "-q", "-m", "base") + self.first = run("rev-parse", "HEAD") + write("core/libs/prx/libTwo/Two.cpp", export("sceTwoSecond")) + run("commit", "-qam", "change two") + self.second = run("rev-parse", "HEAD") + self.cache = Path(run("rev-parse", "--absolute-git-dir")) / "pr_overlap-exports.json" + self.parsed = [] + real = pr_overlap.source_exports + + def counting(text, library=""): + self.parsed.append(library) + return real(text, library) + + patcher = patch.object(pr_overlap, "source_exports", side_effect=counting) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + os.chdir(self.old) + self.tmp.cleanup() + + def names(self, found): + return {name for names in found.values() for name in names} + + def test_matches_the_sources_without_tests_or_headers(self): + found = pr_overlap.base_exports(self.first) + self.assertEqual(self.names(found), {"sceOneFirst", "sceTwoFirst"}) + self.assertEqual({library for library, _ in found}, {"core/libs/prx/libOne", "core/libs/prx/libTwo"}) + + def test_a_second_run_on_the_same_base_parses_nothing(self): + first = pr_overlap.base_exports(self.first) + self.assertEqual(len(self.parsed), 2) + self.assertEqual(pr_overlap.base_exports(self.first), first) + self.assertEqual(len(self.parsed), 2) + + def test_a_moved_base_parses_only_the_changed_sources(self): + pr_overlap.base_exports(self.first) + self.parsed.clear() + found = pr_overlap.base_exports(self.second) + self.assertEqual(self.parsed, ["core/libs/prx/libTwo"]) + self.assertEqual(self.names(found), {"sceOneFirst", "sceTwoFirst", "sceTwoSecond"}) + + def test_switching_back_to_an_older_base_reuses_its_entries(self): + pr_overlap.base_exports(self.first) + pr_overlap.base_exports(self.second) + self.parsed.clear() + pr_overlap.base_exports(self.first) + self.assertEqual(self.parsed, []) + + def test_a_cache_from_other_parsers_is_ignored(self): + pr_overlap.base_exports(self.first) + cache = json.loads(self.cache.read_text(encoding="utf-8")) + cache["version"] = "older parsers" + self.cache.write_text(json.dumps(cache), encoding="utf-8") + self.parsed.clear() + pr_overlap.base_exports(self.first) + self.assertEqual(len(self.parsed), 2) + + def test_an_unreadable_cache_is_rebuilt(self): + self.cache.write_text("{not json", encoding="utf-8") + found = pr_overlap.base_exports(self.first) + self.assertEqual(self.names(found), {"sceOneFirst", "sceTwoFirst"}) + self.assertEqual(json.loads(self.cache.read_text(encoding="utf-8"))["version"], pr_overlap.export_cache_version()) + + def test_a_cache_that_cannot_be_written_does_not_fail_the_run(self): + with patch.object(pr_overlap.os, "replace", side_effect=PermissionError("read-only")): + found = pr_overlap.base_exports(self.first) + self.assertEqual(self.names(found), {"sceOneFirst", "sceTwoFirst"}) + self.assertEqual([path.name for path in self.cache.parent.glob("pr_overlap-exports.json*")], []) + + +if __name__ == "__main__": + unittest.main() From 389ff88dffc3146c6fa17afc568348bbee8c8bdc Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:57:38 +0000 Subject: [PATCH 077/702] build: register the pr_overlap export cache test --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 40351cb867..d127b69098 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -231,6 +231,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_progress.py) add_test(NAME pr_overlap_pages COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_pages.py) + add_test(NAME pr_overlap_export_cache COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_export_cache.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} From 2ca76b589c2ab9e9ff62e371e19ab981ab1c924d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:57:57 +0000 Subject: [PATCH 078/702] docs(tools): mention pr_overlap's export cache in its help --- tools/pr_overlap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 983e5ab363..c655dd6e88 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -498,7 +498,10 @@ def main(argv=None): sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") if hasattr(sys.stderr, "reconfigure"): sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") - parser = argparse.ArgumentParser(description="report open pull requests that overlap each other") + parser = argparse.ArgumentParser( + description="report open pull requests that overlap each other", + epilog="The exports found in the base are cached by source blob in pr_overlap-exports.json inside the git " + "directory; deleting that file only makes the next run slower.") parser.add_argument("--base", default="main") parser.add_argument("--out", type=Path, help="write .md for every open pull request (empty when clean)") parser.add_argument("--branch", help="check a local branch against the open pull requests instead") From 73fffbee8e5f630d7a57dba5b379019f4b2cbb0b Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:13:23 -0400 Subject: [PATCH 079/702] fix(libkernel): refuse undefined protection bits in the map calls with EINVAL as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/DirectMemory/Export.cpp | 9 +++++++++ core/libs/tests/GuestMemory.cpp | 15 +++++++-------- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 93101cb207..55ec619930 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -50,6 +50,10 @@ size_t _configuredFlexibleSize() { return size; } +bool UndefinedProtection(int prot) { + return (prot & ~0x3F7) != 0; +} + constexpr int PRT_APERTURE_COUNT = 3; struct PrtAperture { @@ -258,6 +262,7 @@ int APS5_VABI sceKernelEnableDmemAliasing(void) { } int APS5_VABI sceKernelMapDirectMemory(void** addr, size_t len, int prot, int flags, int64_t direct_memory_start, size_t alignment) { + if (UndefinedProtection(prot)) return SCE_KERNEL_ERROR_EINVAL; (void)alignment; const int result = DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); @@ -265,6 +270,7 @@ int APS5_VABI sceKernelMapDirectMemory(void** addr, size_t len, int prot, int fl } int APS5_VABI sceKernelMapDirectMemory2(void** addr, size_t len, int type, int prot, int flags, int64_t direct_memory_start, size_t alignment) { + if (UndefinedProtection(prot)) return SCE_KERNEL_ERROR_EINVAL; (void)type; const int result = DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); @@ -272,11 +278,13 @@ int APS5_VABI sceKernelMapDirectMemory2(void** addr, size_t len, int type, int p } int APS5_VABI sceKernelMapFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags) { + if (UndefinedProtection(prot)) return SCE_KERNEL_ERROR_EINVAL; constexpr int IgnoredHeapFlag = 0x8000; return _mapFlexible(addr_in_out, len, prot, flags & ~IgnoredHeapFlag); } int APS5_VABI sceKernelMapNamedDirectMemory(void** addr, size_t len, int prot, int flags, int64_t direct_memory_start, size_t alignment, const char* name) { + if (UndefinedProtection(prot)) return SCE_KERNEL_ERROR_EINVAL; const int result = DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); if (result == 0 && name) sceKernelSetVirtualRangeName(*addr, len, name); @@ -284,6 +292,7 @@ int APS5_VABI sceKernelMapNamedDirectMemory(void** addr, size_t len, int prot, i } int32_t APS5_VABI sceKernelMapNamedFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags, const char* name) { + if (UndefinedProtection(prot)) return SCE_KERNEL_ERROR_EINVAL; const int result = _mapFlexible(addr_in_out, len, prot, flags); if (result == 0 && name) sceKernelSetVirtualRangeName(*addr_in_out, len, name); return result; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 7f78313a05..38e0203ee2 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -182,10 +182,8 @@ static void CheckAudioCoprocessorProtection() { Require(sceKernelMprotect(writable, length, 0x3f2) == 0); static_cast(writable)[0] = 9; Require(sceKernelMunmap(reinterpret_cast(writable), length) == 0); - bool rejected = false; void* undefined = nullptr; - try { sceKernelMapFlexibleMemory(&undefined, length, 0x400, 0); } catch (const std::invalid_argument&) { rejected = true; } - Require(rejected && undefined == nullptr); + Require(sceKernelMapFlexibleMemory(&undefined, length, 0x400, 0) == SCE_KERNEL_ERROR_EINVAL && undefined == nullptr); } static void CheckMprotectIgnoresUndefinedBits() { @@ -247,14 +245,15 @@ static void CheckStrictMemoryProtectionBits() { }; std::size_t available = 0; Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); - for (const int protection : {-256, -129, -128, -14, -1, 8, 0x400, std::numeric_limits::min()}) { + for (const int protection : {-256, -129, -128, -14, -1, 8, 0x400, 0x800, 0x3ff, std::numeric_limits::min()}) { void* invalid = nullptr; - rejected([&] { sceKernelMapFlexibleMemory(&invalid, length, protection, 0); }); - Require(invalid == nullptr); + Require(sceKernelMapFlexibleMemory(&invalid, length, protection, 0) == SCE_KERNEL_ERROR_EINVAL && invalid == nullptr); + Require(sceKernelMapNamedFlexibleMemory(&invalid, length, protection, 0, "undefined") == SCE_KERNEL_ERROR_EINVAL && invalid == nullptr); std::size_t after = 0; Require(sceKernelAvailableFlexibleMemorySize(&after) == 0 && after == available); - rejected([&] { sceKernelMapDirectMemory(&invalid, length, protection, 0, physical, 0); }); - Require(invalid == nullptr); + Require(sceKernelMapDirectMemory(&invalid, length, protection, 0, physical, 0) == SCE_KERNEL_ERROR_EINVAL && invalid == nullptr); + Require(sceKernelMapDirectMemory2(&invalid, length, 0, protection, 0, physical, 0) == SCE_KERNEL_ERROR_EINVAL && invalid == nullptr); + Require(sceKernelMapNamedDirectMemory(&invalid, length, protection, 0, physical, 0, "undefined") == SCE_KERNEL_ERROR_EINVAL && invalid == nullptr); rejected([&] { sceKernelMtypeprotect(mapping, length, 2, protection); }); VirtualQueryInfo info{}; Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4e1851fb4e..3babf347a5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -134,7 +134,7 @@ - [Linux runtime module initialization](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - modules in explicit runtime directories can be mapped for bindings before an explicit guest load runs their entry and init array once with the caller arguments. Finalizers run only after initialization completed. Console dependency-start and finalization ordering are unmeasured. Windows and macOS use their existing lifecycle paths. - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. -- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute +- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here `sceKernelMapFlexibleMemory`, `sceKernelMapNamedFlexibleMemory`, `sceKernelMapDirectMemory`, `sceKernelMapDirectMemory2` and `sceKernelMapNamedDirectMemory` return `EINVAL` for them as well, before changing any mapping, while `sceKernelMtypeprotect` and the batch map calls throw for them, and the direct map calls map execute - [JIT shared memory on macOS](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - the object is an unlinked `mkstemp` file in the temporary directory, as `shm_open` makes its anonymous objects. macOS refuses an executable shared file mapping (`EPERM`), so an executable view is mapped without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view after it ran from another keeps its old translation until the executable view's protection changes; first writes and a single read-write-execute view, as XPSemu PPSA97358 and VLC PPSA85300 map, are seen. `mmap` of a JIT descriptor is not supported, as file-backed guest `mmap` throws on macOS. - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing here is taken from a console except what the kevent measurements entry lists. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters except `EVFILT_AIO`, `EVFILT_LIO`, `EVFILT_SENDFILE` and `EVFILT_PROCDESC` (see the kevent measurements entry), `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry) and every change to an existing timer except `EV_DELETE` and the `EV_ADD` re-adds that the kevent measurements entry describes throw. A `NOTE_NSECONDS` period above one second with a fraction is kept to the nanosecond, where 11.0 converts the fraction with the microsecond macro. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw From d9e8185cc11085274d56fc03115541e3d43c813c Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:35:16 +0100 Subject: [PATCH 080/702] refactor(libc): read through CopyGuestBytes in GuestCopyIn The last branch of GuestCopyIn_nid_no_patch was a plain memcpy that reported success, so an unreadable pointer on any other host ended the process. Keep the null, overflow and zero-length guards and make exactly one call to CopyGuestBytes, which reads with ReadProcessMemory, process_vm_readv or mach_vm_read_overwrite and throws on a host without a checked read. The per-platform branches are removed. On Windows this replaces the VirtualQuery walk and memcpy with ReadProcessMemory. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/General.cpp | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index ae36b25693..3ef82c1dbb 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -841,34 +841,7 @@ extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, if (bytes == 0) return true; const auto start = reinterpret_cast(source); if (start == 0 || bytes > UINTPTR_MAX - start) return false; - GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); -#ifdef _WIN32 - for (auto address = start; address < start + bytes;) { - MEMORY_BASIC_INFORMATION memory{}; - if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory) || memory.State != MEM_COMMIT || - (memory.Protect & PAGE_GUARD) != 0 || - (memory.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) == 0) return false; - address = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; - } - std::memcpy(destination, source, bytes); - return true; -#elif defined(__linux__) - iovec local{destination, bytes}; - iovec remote{const_cast(source), bytes}; - const ssize_t copied = ::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0); - if (copied < 0) { - if (errno == EFAULT) return false; - throw std::runtime_error("GuestCopyIn: process_vm_readv failed with errno " + std::to_string(errno)); - } - return static_cast(copied) == bytes; -#elif defined(__APPLE__) - mach_vm_size_t copied = 0; - return ::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(source), bytes, - reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; -#else - std::memcpy(destination, source, bytes); - return true; -#endif + return CopyGuestBytes(destination, source, bytes); } extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { From 46566d2d032f312d2775d2e70033a050ea0aa3d5 Mon Sep 17 00:00:00 2001 From: plexerPK <160128888+plexerPK@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:18:13 +0200 Subject: [PATCH 081/702] fix(relinker): preserve opcode-encoded stack register uses --- .../codegen/src/x86/RedZoneFramer.cpp | 9 ++++- .../codegen/tests/RedZoneFramerTests.cpp | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/src/x86/RedZoneFramer.cpp b/core/relinker/codegen/src/x86/RedZoneFramer.cpp index cfe714faef..4cfaeccd24 100644 --- a/core/relinker/codegen/src/x86/RedZoneFramer.cpp +++ b/core/relinker/codegen/src/x86/RedZoneFramer.cpp @@ -119,7 +119,14 @@ bool vexGeneralUsesRsp(const std::uint8_t* code, const Insn& insn, const std::ui MemoryUse memoryUse(const std::uint8_t* code, const Insn& insn) { MemoryUse use; const auto& info = insn.Info; - if (!info.HasModRm || info.ModRmOffset == 0 || info.ModRmOffset >= insn.Length) return use; + if (!info.HasModRm) { + const auto op = info.Opcode; + if (!info.IsTwoByteOpcode && (info.RexPrefix & 1) == 0 + && (op == 0x54 || op == 0x5c || op == 0x94 || op == 0xbc || (op == 0xb4 && info.RexPrefix != 0))) + use.UsesRsp = use.RegisterRsp = true; + return use; + } + if (info.ModRmOffset == 0 || info.ModRmOffset >= insn.Length) return use; bool r = false; bool x = false; bool b = false; diff --git a/core/relinker/codegen/tests/RedZoneFramerTests.cpp b/core/relinker/codegen/tests/RedZoneFramerTests.cpp index 31466fcfdb..1004f468f3 100644 --- a/core/relinker/codegen/tests/RedZoneFramerTests.cpp +++ b/core/relinker/codegen/tests/RedZoneFramerTests.cpp @@ -285,6 +285,42 @@ void skipsLeavesWithOperandsThatCannotMove() { require(report.Skipped.at("prologue shorter than a jump") == 1, "the leaf with a short prologue was not reported with its reason"); } +void skipsOpcodeEncodedRspUses() { + const std::vector> uses{ + {0x54, 0x58}, + {0x66, 0x54, 0x66, 0x58}, + {0x40, 0xb4, 0x10}, + {0x66, 0xbc, 0x00, 0x10}, + {0x54, 0x5c}, + {0x48, 0x94, 0x48, 0x94}, + {0xbc, 0x00, 0x10, 0x00, 0x00}, + {0x48, 0xbc, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + }; + for (const auto& use : uses) { + auto body = StoreBelowRsp; + append(body, use); + const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(sites.empty() && displacements.empty() && report.Skipped.at("other use of rsp") == 1, + "an opcode-encoded rsp use was framed"); + } +} + +void framesOpcodeEncodedNonStackRegisters() { + for (const auto& use : std::vector>{ + {0x49, 0x94, 0x49, 0x94}, {0x41, 0xbc, 0x00, 0x10, 0x00, 0x00}, {0xb4, 0x10}}) { + auto body = StoreBelowRsp; + append(body, use); + const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && sites.size() == 2, "an opcode-encoded non-stack register prevented framing"); + } +} + void skipsLeavesThatCopyRsp() { std::vector withCmov = StoreBelowRsp; append(withCmov, {0x48, 0x0f, 0x45, 0xc4}); @@ -609,6 +645,8 @@ int main() { skipsShortOperandsAtRspWhoseWidenedStubCoversABranchTarget(); skipsLeavesWithOperandsThatCannotMove(); skipsLeavesThatCopyRsp(); + skipsOpcodeEncodedRspUses(); + framesOpcodeEncodedNonStackRegisters(); framesEachFunctionFromItsOwnFde(); treatsATailCallPastTheFdeEndAsLeaving(); framesALeafAfterAJumpWithoutInt3FromItsOwnFde(); From c67997b99bf14ef79e702457f87f732ba62d3e7d Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:51:53 +0100 Subject: [PATCH 082/702] test(libkernel): cover getitimer and setitimer on the non-real timers with an unusable pointer getitimer and setitimer(which != ITIMER_REAL, previous) copy the result out through the checked write in Signals.cpp; the fault test only exercised ITIMER_REAL. Add getitimer(ITIMER_VIRTUAL) and setitimer(ITIMER_VIRTUAL, zero, previous) with an unmapped pointer expecting EFAULT, and the matching calls with a valid pointer. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestOutputPointerFault.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestOutputPointerFault.cpp b/core/libs/tests/GuestOutputPointerFault.cpp index f299c106ec..6877c50c33 100644 --- a/core/libs/tests/GuestOutputPointerFault.cpp +++ b/core/libs/tests/GuestOutputPointerFault.cpp @@ -84,12 +84,18 @@ int main() { Require(Efault(setitimer_nid_postfix(0, zeroTimerValue, badTimer))); *__error_nid_postfix() = 0; Require(Efault(setitimer_nid_postfix(0, badTimer, timerValid))); + *__error_nid_postfix() = 0; + Require(Efault(getitimer_nid_postfix(1, badTimer))); + *__error_nid_postfix() = 0; + Require(Efault(setitimer_nid_postfix(1, zeroTimerValue, badTimer))); Require(sceKernelGettimeofday(badTime) == KernelEfault); Require(sceKernelGettimezone(badZone) == KernelEfault); } Require(gettimeofday_nid_postfix(timeValid, zoneValid) == 0); Require(getitimer_nid_postfix(0, timerValid) == 0); Require(setitimer_nid_postfix(0, zeroTimerValue, timerValid) == 0); + Require(getitimer_nid_postfix(1, timerValid) == 0); + Require(setitimer_nid_postfix(1, zeroTimerValue, timerValid) == 0); GuestItimerval armed{}; armed.value.tv_sec = 3600; *__error_nid_postfix() = 0; From 6247fb1dd6ae5847d386f9d55f314c788079d662 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:07:14 +0100 Subject: [PATCH 083/702] fix(relinker): validate executable export names and attributes like guest modules The executable export loop accepted names that are empty after stripping '#', IFUNC, FILE, COMMON and SECTION types, bindings above STB_WEAK, st_other bits above the visibility, and same-name exports that differ, and Linux copied them into .dynsym. It now rejects them before the location checks with the rule GuestImageReader applies to non-local guest symbols, while identical aliases stay accepted. Every target reports the same line at the symbol entry offset, and the macOS attribute and empty-name checks, now unreachable, are removed. --- core/relinker/CMakeLists.txt | 2 + .../elfpatcher/general/ElfConstants.hpp | 1 + .../src/macos/MacOsMachOPatcher.cpp | 4 - .../src/pipeline/RelinkerPipeline.cpp | 8 ++ .../test_executable_export_attributes.py | 94 +++++++++++++++++++ .../tests/test_macos_executable_exports.py | 2 +- 6 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 core/relinker/relinker/tests/test_executable_export_attributes.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index dd1f8daae6..5ccde83548 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -578,6 +578,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $) add_test(NAME executable_export_bounds COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_export_bounds.py $) + add_test(NAME executable_export_attributes COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_export_attributes.py $) add_test(NAME macos_empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_empty_tls.py $) set_tests_properties(macos_empty_tls PROPERTIES TIMEOUT 60) diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index b8b6b853b6..a3defa240e 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -152,6 +152,7 @@ inline constexpr std::uint32_t R_X86_64_IRELATIVE = 37; inline constexpr std::uint8_t STB_GLOBAL = 1; inline constexpr std::uint8_t STB_WEAK = 2; inline constexpr std::uint8_t STT_NOTYPE = 0; +inline constexpr std::uint8_t STT_OBJECT = 1; inline constexpr std::uint8_t STT_FUNC = 2; inline constexpr std::uint8_t STT_TLS = 6; inline constexpr std::uint8_t STV_DEFAULT = 0; diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index ff62ba6e2d..e47f6ad575 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -78,11 +78,7 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou const auto info = symbols[offset + 4]; const auto visibility = symbols[offset + 5] & 3; if (section == SHN_UNDEF || (info >> 4) == 0 || visibility == 1 || visibility == 2) continue; - if (((info >> 4) != STB_GLOBAL && (info >> 4) != STB_WEAK) || - ((info & 15) != STT_NOTYPE && (info & 15) != 1 && (info & 15) != STT_FUNC && (info & 15) != STT_TLS)) - throw Domain::RelinkerException("Unsupported macOS executable export attributes"); const auto name = "_" + Windows::ReadString(dynamicSection.DynStrData, Io::ReadU32(symbols, offset)); - if (name.size() == 1) throw Domain::RelinkerException("Empty macOS executable export"); const auto value = Io::ReadU64(symbols, offset + 8); const auto flags = (info >> 4) == STB_WEAK ? ExportWeakDefinition : std::uint64_t{0}; if ((info & 15) == STT_TLS) { diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 1dec1d5a25..83f5f4adf4 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -427,6 +428,7 @@ RelinkResult RelinkerPipeline::Relink() { const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ); if (symbolBytes % symEntSize != 0 || dynSymTabOffset > raw.size() || symbolBytes > raw.size() - dynSymTabOffset) throw RelinkerException("Invalid executable dynamic symbol table size"); + std::map exportEntries; for (ByteCount off = symEntSize; off < symbolBytes; off += symEntSize) { const auto* symbol = raw.data() + dynSymTabOffset + off; std::uint16_t section = 0; @@ -438,6 +440,12 @@ RelinkResult RelinkerPipeline::Relink() { name.resize(name.find('#') == std::string::npos ? name.size() : name.find('#')); const FileByteOffset entryOffset = dynSymTabOffset + off; const auto type = symbol[4] & 15; + if (name.empty()) + throw RelinkerException("Empty executable export name", entryOffset); + if ((symbol[4] >> 4) > STB_WEAK || (symbol[5] & ~3) != 0 || (type != STT_NOTYPE && type != STT_OBJECT && type != STT_FUNC && type != STT_TLS)) + throw RelinkerException("Unsupported executable export attributes: " + name, entryOffset); + if (const auto [previous, inserted] = exportEntries.emplace(name, symbol); !inserted && std::memcmp(previous->second + 4, symbol + 4, symEntSize - 4) != 0) + throw RelinkerException("Conflicting executable exports after stripping #: " + name, entryOffset); std::uint64_t value = 0; std::uint64_t size = 0; std::memcpy(&value, symbol + 8, sizeof(value)); diff --git a/core/relinker/relinker/tests/test_executable_export_attributes.py b/core/relinker/relinker/tests/test_executable_export_attributes.py new file mode 100644 index 0000000000..3a29d34ae7 --- /dev/null +++ b/core/relinker/relinker/tests/test_executable_export_attributes.py @@ -0,0 +1,94 @@ +"""An exported symbol of the executable must carry a name and attributes that GuestImageReader accepts for a +non-local symbol of a guest module. A defined symbol that is not local, hidden or internal needs a name that is +not empty after stripping the '#' suffixes, a global or weak binding, no st_other bits above the visibility, and +the type STT_NOTYPE, STT_OBJECT, STT_FUNC or STT_TLS. Two such symbols with the same stripped name must agree in +every byte after st_name; identical aliases stay accepted. Local, hidden and internal symbols are not checked. +The check runs before any target-specific code, so an export it rejects fails with the same line, at the +symbol entry's file offset, on every target.""" + +import struct +import tempfile +from pathlib import Path +import sys + +from test_executable_export_bounds import FUNC, LOCAL, NOTYPE, SYMBOL_OFFSET, TARGETS, fail_lines, relink +from test_executable_export_bounds import image as single_symbol_image + +GLOBAL_FUNC, GLOBAL_OBJECT, WEAK_OBJECT = 0x12, 0x11, 0x21 +ATTRIBUTES = "Unsupported executable export attributes: {}" +CONFLICT = "Conflicting executable exports after stripping #: {}" +EMPTY = "Empty executable export name" + +REJECTED = [ + ("empty_name", [("", GLOBAL_FUNC, 0, 0x200, 6)], EMPTY), + ("hash_only_name", [("#A#B", GLOBAL_FUNC, 0, 0x200, 6)], EMPTY), + ("ifunc", [("Indirect", 0x1A, 0, 0x200, 6)], ATTRIBUTES.format("Indirect")), + ("file", [("File", 0x14, 0, 0x1100, 8)], ATTRIBUTES.format("File")), + ("common", [("Common", 0x15, 0, 0x1100, 8)], ATTRIBUTES.format("Common")), + ("global_section", [("Section", 0x13, 0, 0x1100, 8)], ATTRIBUTES.format("Section")), + ("binding_3", [("Binding", 0x31, 0, 0x1100, 8)], ATTRIBUTES.format("Binding")), + ("gnu_unique", [("Unique", 0xA1, 0, 0x1100, 8)], ATTRIBUTES.format("Unique")), + ("other_high_bits", [("Other", GLOBAL_OBJECT, 0x10, 0x1100, 8)], ATTRIBUTES.format("Other")), + ("different_values", [("foo", GLOBAL_OBJECT, 0, 0x1100, 8), ("foo", GLOBAL_OBJECT, 0, 0x1108, 8)], + CONFLICT.format("foo")), + ("suffix_different_value", [("foo", GLOBAL_OBJECT, 0, 0x1100, 8), ("foo#A#B", GLOBAL_OBJECT, 0, 0x1108, 8)], + CONFLICT.format("foo")), + ("global_and_weak", [("foo", GLOBAL_OBJECT, 0, 0x1100, 8), ("foo", WEAK_OBJECT, 0, 0x1100, 8)], + CONFLICT.format("foo")), + ("functions_apart", [("fn", GLOBAL_FUNC, 0, 0x200, 6), ("fn#A#B", GLOBAL_FUNC, 0, 0x206, 6)], + CONFLICT.format("fn")), +] + +ACCEPTED = [ + ("identical_functions", [("fn", GLOBAL_FUNC, 0, 0x200, 6), ("fn#A#B", GLOBAL_FUNC, 0, 0x200, 6)], + "Duplicate Mach-O export"), + ("identical_objects", [("obj", GLOBAL_OBJECT, 0, 0x1100, 8), ("obj", GLOBAL_OBJECT, 0, 0x1100, 8)], + "Duplicate Mach-O export"), + ("protected_object", [("Protected", GLOBAL_OBJECT, 3, 0x1100, 8)], None), + ("local_ifunc", [("LocalIndirect", 0x0A, 0, 0x200, 6)], None), +] + + +def image(symbols): + data = single_symbol_image("", NOTYPE, 0, 0, LOCAL, 0, 0, False) + strings = b"\0" + for index, (name, info, other, value, size) in enumerate(symbols): + name_offset = len(strings) if name else 0 + if name: + strings += name.encode("ascii") + b"\0" + struct.pack_into(" Date: Sun, 25 Oct 2026 14:25:31 +0100 Subject: [PATCH 084/702] fix(relinker): reject an executable dynamic string table that does not begin with NUL The pipeline read names at string offset 0 without checking the first byte of DT_STRTAB, so DT_NEEDED 0 or an st_name of 0 named a library or an import after the first string, and every target relinked it. The gABI reserves index 0 of a string table for a NUL byte that means no name, and GuestImageReader already rejects modules whose table breaks this rule. Reading offset 0 from such a table now fails with "Dynamic string table does not begin with a NUL byte" and writes no output. --- .../src/pipeline/RelinkerPipeline.cpp | 2 ++ .../tests/test_executable_segments.py | 5 +++-- .../relinker/tests/test_needed_libraries.py | 20 ++++++++++++++++--- .../relinker/tests/test_optional_plt.py | 5 +++-- .../tests/test_unused_filter_indirect.py | 5 +++-- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 1dec1d5a25..91033e3493 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -265,6 +265,8 @@ RelinkResult RelinkerPipeline::Relink() { auto readCStr = [&](FileByteOffset strOff) -> std::string { if (strOff >= dynStrTabSize) throw RelinkerException("Dynamic string offset is outside DT_STRSZ", strOff); + if (strOff == 0 && raw[dynStrTabOffset] != 0) + throw RelinkerException("Dynamic string table does not begin with a NUL byte", dynStrTabOffset); std::string result; FileByteOffset pos = dynStrTabOffset + strOff; const FileByteOffset end = dynStrTabOffset + dynStrTabSize; diff --git a/core/relinker/relinker/tests/test_executable_segments.py b/core/relinker/relinker/tests/test_executable_segments.py index 34e3d1ae4b..82e36bc9cb 100644 --- a/core/relinker/relinker/tests/test_executable_segments.py +++ b/core/relinker/relinker/tests/test_executable_segments.py @@ -16,13 +16,14 @@ def fixture(second_code: bytes, with_import: bool = False) -> bytearray: image[0x210:0x215] = b"\xe9\xfb\x05\x00\x00" image[0x800:0x800 + len(second_code)] = second_code struct.pack_into(" Date: Sun, 25 Oct 2026 10:34:14 -0400 Subject: [PATCH 085/702] fix(audioout2): poll device queue levels without the callback mutex --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 2 +- .../libSceAudioOut/src/AudioOut2Context.cpp | 25 +++++++++-- .../libSceAudioOut/src/AudioOut2Internal.hpp | 3 ++ .../libSceAudioOut/tests/AudioOut2Timing.cpp | 43 +++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 7c97d07a55..005ef4485a 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -224,7 +224,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test_executable(audio_out2_timing_tests tests/AudioOut2Timing.cpp) - target_include_directories(audio_out2_timing_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_include_directories(audio_out2_timing_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(audio_out2_timing_tests PRIVATE libSceAudioOut libc) configure_windows_unwind(audio_out2_timing_tests) add_test(NAME audio_out2_timing COMMAND audio_out2_timing_tests) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 905eaa42f5..82cc45c892 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -93,14 +93,22 @@ static void Drain(AudioOut2Context& context, Clock::time_point now) { } // Grains queued and not yet played, as the title sees them. The caller holds the context lock. -static std::uint32_t QueueLevel(AudioOut2Context& context, Clock::time_point now) { - Drain(context, now); - if (context.device == 0) return context.queued; +static std::uint32_t DeviceQueueLevel(const AudioOut2Context& context) { const auto unplayed = UnplayedGrains(context); const auto cushion = CushionGrains(context); return unplayed > cushion ? std::min(context.queueDepth, unplayed - cushion) : 0; } +static std::uint32_t QueueLevel(AudioOut2Context& context, Clock::time_point now) { + Drain(context, now); + return context.device == 0 ? context.queued : DeviceQueueLevel(context); +} + +static void PublishQueueLevel(AudioOut2Context& context) { + const auto level = std::min(context.queueDepth, DeviceQueueLevel(context) + static_cast(context.advancedGrains.size())); + context.publishedQueueLevel.store(level, std::memory_order_release); +} + static void DeviceCallback(void* userdata, Uint8* stream, int length); static void OpenDevice(AudioOut2Context& context) { @@ -234,6 +242,7 @@ static void DeviceCallback(void* userdata, Uint8* stream, int length) { context.priming = true; context.primes++; } + PublishQueueLevel(context); } static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { @@ -274,6 +283,7 @@ int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { context->advancedMode = true; } context->advancedGrains.push_back(AudioOut2CaptureGrain(*context, true)); + PublishQueueLevel(*context); context->grainReady.notify_one(); } context->advances++; @@ -295,6 +305,7 @@ int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, voi AudioOut2TraceSeconds(), params->max_ports, params->max_object_ports, params->guarantee_object_ports, params->queue_depth, params->num_grains, params->flags, buffer, buffer_size, static_cast(context), context->grain, 1000.0 * context->grain / AUDIO_OUT2_SAMPLE_RATE, context->queueDepth, AUDIO_OUT2_SAMPLE_RATE); OpenDevice(*context); + context->deviceBacked = context->device != 0; *ctx = reinterpret_cast(context); return 0; } @@ -321,6 +332,12 @@ int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) try { int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue) { auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + if (context->deviceBacked && !AudioOut2TraceEnabled()) { + const auto level = context->publishedQueueLevel.load(std::memory_order_acquire); + if (queue_level) *queue_level = level; + if (available_queue) *available_queue = context->queueDepth - level; + return 0; + } std::lock_guard lock(context->lock); const auto level = std::min(context->queueDepth, QueueLevel(*context, Clock::now()) + static_cast(context->advancedGrains.size())); if (queue_level) *queue_level = level; @@ -374,6 +391,7 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block while (context->pendingGrains.size() >= 2) { const auto readAt = context->pendingSince.front() + READ_DELAY; if (Clock::now() < readAt) { + PublishQueueLevel(*context); lock.unlock(); while (Clock::now() < readAt) std::this_thread::yield(); lock.lock(); @@ -389,6 +407,7 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); } } + if (context->device != 0) PublishQueueLevel(*context); context->pushes++; context->summaryPushes++; if (blocking) context->blockingPushes++; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 42363a9998..d7c96728fc 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP #include +#include #include #include #include @@ -116,6 +117,8 @@ struct AudioOut2Context { // Largest sample magnitude of the grains mixed since the last summary: the mixed signal's level. float summaryPeak = 0.0f; std::chrono::steady_clock::time_point summaryStart; + bool deviceBacked = false; + std::atomic publishedQueueLevel{0}; }; // Mixes every port of the context that carries PCM data into out (stereo float, frames frames), summing diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index 0dfd811378..d0b85c57cc 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAudioOut/include/AudioOut2.hpp" +#include "prx/libSceAudioOut/src/AudioOut2Internal.hpp" #include "SceTypes.hpp" #include @@ -9,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -161,6 +163,44 @@ void TestLevelExcludesCushion() { session.Close(); } +void TestDeviceLevelDoesNotWaitForTheContext(bool low) { + Session session("anyps5_audio_out2_level_poll.raw", low); + auto& context = *reinterpret_cast(session.context); + Require(context.device != 0); + std::promise started; + auto ready = started.get_future(); + std::unique_lock lock(context.lock); + auto poll = std::async(std::launch::async, [&] { + started.set_value(); + std::uint32_t level = 99; + std::uint32_t available = 99; + Require((low ? sceAudioOut2LoContextGetQueueLevel : sceAudioOut2ContextGetQueueLevel)(session.context, &level, &available) == 0); + return std::pair{level, available}; + }); + ready.wait(); + const bool completed = poll.wait_for(std::chrono::seconds(1)) == std::future_status::ready; + lock.unlock(); + const auto result = poll.get(); + session.Close(); + Require(completed); + Require(result.first == 0 && result.second == queueDepth); +} + +void TestLevelWithoutADeviceUsesThePlaybackClock() { + AudioOut2Context context; + context.queueDepth = queueDepth; + context.queued = queueDepth; + context.playHead = std::chrono::steady_clock::now() + std::chrono::seconds(5); + const auto handle = reinterpret_cast(&context); + std::uint32_t level = 99; + std::uint32_t available = 99; + Require(sceAudioOut2ContextGetQueueLevel(handle, &level, &available) == 0); + Require(level == queueDepth && available == 0); + context.playHead = std::chrono::steady_clock::now() - std::chrono::seconds(5); + Require(sceAudioOut2LoContextGetQueueLevel(handle, &level, &available) == 0); + Require(level == 0 && available == queueDepth); +} + void TestPrimingAfterRunningDry() { Session session("anyps5_audio_out2_priming.raw"); session.Fill(0.5f); @@ -316,6 +356,9 @@ int main() { TestReadAtNextPush(); TestRecreatedPort(); TestLevelExcludesCushion(); + TestDeviceLevelDoesNotWaitForTheContext(false); + TestDeviceLevelDoesNotWaitForTheContext(true); + TestLevelWithoutADeviceUsesThePlaybackClock(); TestPrimingAfterRunningDry(); TestLowLatencyCallsRejectBadArguments(); TestLowLatencyReadsAtNextPush(); From de2b5de1b76afe51ec909ec9f5991e676edbcf06 Mon Sep 17 00:00:00 2001 From: plexerPK <160128888+plexerPK@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:41:22 +0100 Subject: [PATCH 086/702] fix(relinker): detect opcode-encoded bswap stack operands --- core/relinker/codegen/src/x86/RedZoneFramer.cpp | 5 +++-- core/relinker/codegen/tests/RedZoneFramerTests.cpp | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/relinker/codegen/src/x86/RedZoneFramer.cpp b/core/relinker/codegen/src/x86/RedZoneFramer.cpp index 4cfaeccd24..6fe4dfe256 100644 --- a/core/relinker/codegen/src/x86/RedZoneFramer.cpp +++ b/core/relinker/codegen/src/x86/RedZoneFramer.cpp @@ -121,8 +121,9 @@ MemoryUse memoryUse(const std::uint8_t* code, const Insn& insn) { const auto& info = insn.Info; if (!info.HasModRm) { const auto op = info.Opcode; - if (!info.IsTwoByteOpcode && (info.RexPrefix & 1) == 0 - && (op == 0x54 || op == 0x5c || op == 0x94 || op == 0xbc || (op == 0xb4 && info.RexPrefix != 0))) + if ((info.RexPrefix & 1) == 0 + && ((!info.IsTwoByteOpcode && (op == 0x54 || op == 0x5c || op == 0x94 || op == 0xbc || (op == 0xb4 && info.RexPrefix != 0))) + || (info.IsTwoByteOpcode && op == 0xcc))) use.UsesRsp = use.RegisterRsp = true; return use; } diff --git a/core/relinker/codegen/tests/RedZoneFramerTests.cpp b/core/relinker/codegen/tests/RedZoneFramerTests.cpp index 1004f468f3..246d7676d9 100644 --- a/core/relinker/codegen/tests/RedZoneFramerTests.cpp +++ b/core/relinker/codegen/tests/RedZoneFramerTests.cpp @@ -287,6 +287,9 @@ void skipsLeavesWithOperandsThatCannotMove() { void skipsOpcodeEncodedRspUses() { const std::vector> uses{ + {0x48, 0x0f, 0xcc, 0x48, 0x0f, 0xcc}, + {0x0f, 0xcc, 0x0f, 0xcc}, + {0x4c, 0x0f, 0xcc, 0x4c, 0x0f, 0xcc}, {0x54, 0x58}, {0x66, 0x54, 0x66, 0x58}, {0x40, 0xb4, 0x10}, @@ -310,6 +313,7 @@ void skipsOpcodeEncodedRspUses() { void framesOpcodeEncodedNonStackRegisters() { for (const auto& use : std::vector>{ + {0x49, 0x0f, 0xcc, 0x49, 0x0f, 0xcc}, {0x41, 0x0f, 0xcc, 0x41, 0x0f, 0xcc}, {0x49, 0x94, 0x49, 0x94}, {0x41, 0xbc, 0x00, 0x10, 0x00, 0x00}, {0xb4, 0x10}}) { auto body = StoreBelowRsp; append(body, use); From b78627d35409085ca24f8d9e5e31af96fc1d55e0 Mon Sep 17 00:00:00 2001 From: orezraey Date: Sun, 25 Oct 2026 11:41:31 -0300 Subject: [PATCH 087/702] fix(libkernel): fail the yield loop setter on a destroyed mutex as measured on a PS5 On PS5 firmware 13.60 pthread_mutex_setyieldloops_np and scePthreadMutexSetyieldloops return EINVAL on a destroyed mutex, as the other spin and yield loop calls already did here. The default counts (2000 spins for an adaptive mutex, 0 otherwise, 0 yields) match. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 6 ++++-- core/libs/tests/GuestPthreadMutexErrors.cpp | 4 ++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 84eee73602..db9e7963f0 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -262,8 +262,10 @@ int APS5_VABI scePthreadMutexGetyieldloops(PthreadMutex* mutex, int* count) { } int APS5_VABI scePthreadMutexSetyieldloops(PthreadMutex* mutex, int count) { - if (auto* current = resolveMutex(mutex, true)) - current->_yieldloops = count; + auto* current = resolveMutex(mutex, true); + if (!current) + return SCE_KERNEL_ERROR_EINVAL; + current->_yieldloops = count; return 0; } diff --git a/core/libs/tests/GuestPthreadMutexErrors.cpp b/core/libs/tests/GuestPthreadMutexErrors.cpp index 7d7947f824..6a99ea4c97 100644 --- a/core/libs/tests/GuestPthreadMutexErrors.cpp +++ b/core/libs/tests/GuestPthreadMutexErrors.cpp @@ -101,8 +101,8 @@ static void CheckLoops(PthreadMutexattr* attr) { Require(pthread_mutex_getspinloops_np_nid_postfix(&mutex, &count) == POSIX_EINVAL); Require(pthread_mutex_setspinloops_np_nid_postfix(&mutex, 1) == POSIX_EINVAL); Require(pthread_mutex_getyieldloops_np_nid_postfix(&mutex, &count) == POSIX_EINVAL && count == -1); - Require(scePthreadMutexSetyieldloops(&mutex, 1) == SCE_OK); - Require(pthread_mutex_setyieldloops_np_nid_postfix(&mutex, 1) == 0); + Require(scePthreadMutexSetyieldloops(&mutex, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_mutex_setyieldloops_np_nid_postfix(&mutex, 1) == POSIX_EINVAL); Require(mutex == DestroyedMutex); for (const int type : {1, 2, 3}) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4e1851fb4e..af0c7e21c6 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -269,7 +269,7 @@ - [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same - [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports the host volume of a file, not a console mount: block sizes, block counts, name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0). The type, mount names, owner, filesystem id and I/O counters are 0 or empty. Sockets, pipes and kqueues return `EINVAL`, as FreeBSD's `getvnode` does. A character device, including `/dev/random` and `/dev/urandom`, reports FreeBSD 11.4's [`devfs_statfs`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/fs/devfs/devfs_vfsops.c#L225-L237) values on every host: no flags, 512-byte blocks, 2 blocks, nothing free and a 255-byte name limit. A Windows descriptor that is none of these throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever -- [pthread_mutex_getspinloops_np, pthread_mutex_setspinloops_np, pthread_mutex_getyieldloops_np, pthread_mutex_setyieldloops_np and their scePthreadMutex counterparts](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) store and report the counts, but host mutexes do not spin or yield by them. The defaults follow FreeBSD 11.4's [`mutex_init_body`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L236-L272) without `LIBPTHREAD_SPINLOOPS` and `LIBPTHREAD_YIELDLOOPS`: 2000 spins for an adaptive mutex, 0 otherwise; the console's defaults are unverified. Setting the yield count of a destroyed mutex returns 0 as FreeBSD's [`__pthread_mutex_setyieldloops_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L1125-L1135) does; the console's result is unverified +- [pthread_mutex_getspinloops_np, pthread_mutex_setspinloops_np, pthread_mutex_getyieldloops_np, pthread_mutex_setyieldloops_np and their scePthreadMutex counterparts](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) store and report the counts, but host mutexes do not spin or yield by them. Measured on PS5 firmware 13.60: the defaults are 2000 spins for an adaptive mutex (type 4) and 0 otherwise, with 0 yields, for mutexes from `pthread_mutex_init` and `scePthreadMutexInit` alike, as FreeBSD 11.4's [`mutex_init_body`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L236-L272) sets them; every get and set, the yield setter included, returns `EINVAL` (22, or `0x80020016` from the `scePthreadMutex` calls) on a destroyed mutex without writing the output - [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 - [pthread_mutexattr_setkind_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) throws for a kind outside 1 to 4. FreeBSD 11.4's [`_pthread_mutexattr_setkind_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutexattr.c#L108-L120) stores any kind and [`mutex_check_attr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L207-L218) then makes `pthread_mutex_init` fail with `EINVAL`, but `pthread_mutex_init` here does not check the type - [pthread_cond_signalto_np, scePthreadCondSignalto](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) wake every waiter of the condition variable, not only the given thread, and do not check the thread; the other waiters see a spurious wakeup. Which waiters the console wakes, and its result for a thread that is not waiting, are unverified From c75ef9f48de49fa4712c1ccaa1d068c4cf59fdf6 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 25 Oct 2026 10:52:42 -0400 Subject: [PATCH 088/702] fix(audioout2): publish queue changes when producer scopes exit --- .../libSceAudioOut/src/AudioOut2Context.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 82cc45c892..f6930bbb97 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -109,6 +109,15 @@ static void PublishQueueLevel(AudioOut2Context& context) { context.publishedQueueLevel.store(level, std::memory_order_release); } +struct QueueLevelPublisher { + AudioOut2Context& context; + const std::unique_lock& lock; + + ~QueueLevelPublisher() { + if (lock.owns_lock() && context.device != 0) PublishQueueLevel(context); + } +}; + static void DeviceCallback(void* userdata, Uint8* stream, int length); static void OpenDevice(AudioOut2Context& context) { @@ -229,7 +238,8 @@ static void Render(AudioOut2Context& context, const AudioOut2Grain& grain) { static void DeviceCallback(void* userdata, Uint8* stream, int length) { auto& context = *static_cast(userdata); - std::lock_guard lock(context.lock); + std::unique_lock lock(context.lock); + const QueueLevelPublisher publication{context, lock}; auto* out = reinterpret_cast(stream); const auto samples = static_cast(length) / sizeof(float); const auto cushionSamples = static_cast(CushionGrains(context)) * context.grain * AUDIO_OUT2_OUTPUT_CHANNELS; @@ -242,7 +252,6 @@ static void DeviceCallback(void* userdata, Uint8* stream, int length) { context.priming = true; context.primes++; } - PublishQueueLevel(context); } static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { @@ -271,7 +280,8 @@ int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; - std::lock_guard lock(context->lock); + std::unique_lock lock(context->lock); + const QueueLevelPublisher publication{*context, lock}; if (context->device != 0) { if (!context->advancedMode) { while (!context->pendingGrains.empty()) { @@ -283,7 +293,6 @@ int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { context->advancedMode = true; } context->advancedGrains.push_back(AudioOut2CaptureGrain(*context, true)); - PublishQueueLevel(*context); context->grainReady.notify_one(); } context->advances++; @@ -353,6 +362,7 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::unique_lock lock(context->lock); + const QueueLevelPublisher publication{*context, lock}; auto now = Clock::now(); const auto waitStart = now; while (QueueLevel(*context, now) >= context->queueDepth) { @@ -407,7 +417,6 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); } } - if (context->device != 0) PublishQueueLevel(*context); context->pushes++; context->summaryPushes++; if (blocking) context->blockingPushes++; From 53da58ac911b7053cdf3f2583b79867b73b94a12 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 25 Oct 2026 11:07:39 -0400 Subject: [PATCH 089/702] test(audioout2): preserve the native test entry point with SDL headers --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 005ef4485a..625e336870 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -225,6 +225,7 @@ if(BUILD_TESTING) endif() add_test_executable(audio_out2_timing_tests tests/AudioOut2Timing.cpp) target_include_directories(audio_out2_timing_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_compile_definitions(audio_out2_timing_tests PRIVATE SDL_MAIN_HANDLED) target_link_libraries(audio_out2_timing_tests PRIVATE libSceAudioOut libc) configure_windows_unwind(audio_out2_timing_tests) add_test(NAME audio_out2_timing COMMAND audio_out2_timing_tests) From f1fb9d6eaf3275ab3ed80becf17ba37936a9622d Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Sun, 25 Oct 2026 18:03:07 +0100 Subject: [PATCH 090/702] fix(agc): select the first level of a single-level depth view A DB_DEPTH_VIEW.MIPID above zero on a surface with DB_Z_INFO.MAXMIP zero selected no level and threw. Flashback sets it while it renders the six slices of a single-level 128x128 Z_X depth array, so the field cannot select another level; decode the first one as the console does. --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 3 +-- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 3 ++- docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index da78c29b18..dd8ab874cc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -385,8 +385,8 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint DepthTarget decodeDepthPlane(const Registers& cx) { const auto viewRegister = find(cx, 0x002); const auto view = viewRegister == cx.end() ? 0u : viewRegister->second; - const auto viewMip = (view >> 26u) & 0xfu; const auto maxMip = (read(cx, 0x010) >> 16u) & 0xfu; + const auto viewMip = maxMip == 0 ? 0u : (view >> 26u) & 0xfu; const auto zFormat = read(cx, 0x010) & 3u; const auto stencilRegister = find(cx, 0x011); const auto stencilInfo = stencilRegister == cx.end() ? 0u : stencilRegister->second; @@ -414,7 +414,6 @@ DepthTarget decodeDepthPlane(const Registers& cx) { } const auto size = read(cx, 0x007); depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; - if (maxMip == 0 && viewMip != 0) Require(false, "depth view mip " + std::to_string(viewMip) + " on a surface without mips"); if (maxMip != 0) { char message[320]; std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2595184b0c..958facd658 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1866,7 +1866,8 @@ void DisabledColorTests() { const auto thirdSlice = AgcDriver::Graphics::DecodeState(queue); Require(thirdSlice.depth && thirdSlice.depth->firstSlice == 3 && thirdSlice.depth->address == depthOnly.depth->address + 3u * AgcDriver::Graphics::DepthSliceBytes(depthOnly.depth->extent, 4u), "a depth view of slice 3 lost its slice or its Z offset"); queue.context[0x002] = 1u << 26u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "on a surface without mips"); + const auto singleLevelMip = AgcDriver::Graphics::DecodeState(queue); + Require(singleLevelMip.depth && singleLevelMip.depth->address == depthOnly.depth->address && singleLevelMip.depth->extent.width == depthOnly.depth->extent.width && singleLevelMip.depth->extent.height == depthOnly.depth->extent.height, "a depth view mip on a single-level surface did not select its first level"); queue.context[0x002] = 0; { using AgcDriver::Graphics::ComputeElementMipLayout; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e715b5b83e..5239ba4b01 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -265,6 +265,7 @@ - [Sampled and storage views of 1 GiB or more](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) that stay whole are only logged (sampled views by `cachedTexture`, storage views by `cachedStorageTexture`), with the registered guest range holding their base and how many of their slices fit in it; nothing is capped to that range. PPSA26344 samples a 1467 MiB cube array window of which only 838 of 2934 slices lie inside its 3380 MiB range: the rest is read from whatever follows the range. Whether such slices are ever sampled is not measured. - [Color export type against the target format](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the export-format check refuses a `UINT16_ABGR` export (`SPI_SHADER_COL_FORMAT` 7) into a target whose color-buffer number format is not unsigned integer, but not the reverse. A 32-bit export (`32_R`, `32_GR`, `32_AR` or `32_ABGR`) into an unsigned-integer target leaves `uintExport` clear, so the recompiler declares the fragment output `float4`; `DecodeColorBuffer` maps a number format of 4 to `R32_UINT` from the color buffer alone, independent of the export format, so the float output reaches an integer attachment. Desktop drivers keep the raw bits, but the Khronos validation layer reports `Undefined-Value-ShaderFragmentOutputMismatch` and MoltenVK's Metal compiler refuses the pipeline. Which export format a title pairs with a 32-bit integer target is not measured, so the output type is left following the export format rather than the attachment. - [Depth mip levels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a depth view of mip `DB_DEPTH_VIEW.MIPID` in a chain of `DB_Z_INFO.MAXMIP` levels renders into that level as a surface of its own, at the level's offset and extent from `ComputeElementMipLayout` for the 64 KiB Z_X swizzle, the layout color target mips and sampled depth already use; slices of a chain step by the size of the whole chain. Taken from LEGO Builder's Journey (PPSA05496), which renders depth mip 1 of 64 KiB Z_X surfaces without HTILE; the placement was not compared with a console capture. Multisampled depth mips, other swizzle modes, levels in the packed mip tail and HTILE on a mip chain throw +- [Depth view mip on a single-level surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `DB_DEPTH_VIEW.MIPID` above zero on a surface whose `DB_Z_INFO.MAXMIP` is zero selects the first level. In Flashback (PPSA08546) the depth view follows a mipmapped colour target from mip 1 to 6 over six slices of a 128x128 Z_X depth array; the depth allocation is six 64 KiB slices (the next allocation starts 0x61000 bytes later, too small for a mip tail) and every view uses the same base and extent, so the field has no other level to select. The console was not measured. A depth copy from such a view still throws - [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp): `readHeader` and the failed-read existence check still use ordinary paths. A filename beyond MAX_PATH can prevent header reuse or be reported missing; the shared content reader and journal normalization do not cover these calls. - [DB_RENDER_CONTROL.STENCIL_CLEAR_ENABLE without a stencil plane](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a stencil clear on a depth target whose `DB_STENCIL_INFO` format is `STENCIL_INVALID` is dropped; the depth clear and color writes proceed. AMD PAL ([`gfx9RsrcProcMgr.cpp`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32/src/core/hw/gfxip/rpm/gfx9/gfx9RsrcProcMgr.cpp)) and radeonsi (`si_clear.c`) never set the bit without a stencil plane, and shadPS4 drops it the same way (`vk_rasterizer.cpp`, `StencilValid`). No source states what the hardware does with the bit then; the console was not measured. A read-only stencil plane still throws From d93bb1b3bcf68bb31d7dc3bb095ef5dded2bfdb3 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 25 Oct 2026 16:04:46 -0400 Subject: [PATCH 091/702] fix(agc): verify the effective user words of relocated draws --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++++- .../prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp | 5 +++++ .../libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0b65806855..1e5b358ed3 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2583,12 +2583,15 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_sparse_indirect_indices_unreadable agc_driver_sparse_indirect_indices_no_size agc_driver_sparse_indirect_indices_no_size_unreadable agc_driver_sparse_indirect_indices_no_size_unreadable_record PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_draw_cache_relocation_recipe_verify COMMAND agc_driver_draw_cache_relocation_tests) + set_tests_properties(agc_driver_draw_cache_relocation_recipe_verify PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_VERIFY_DRAW_RECIPE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77 TIMEOUT 60 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_draw_cache_relocation_evicted COMMAND agc_driver_draw_cache_relocation_tests --evicted) add_test(NAME agc_driver_draw_cache_relocation_kept COMMAND agc_driver_draw_cache_relocation_tests --kept) set_tests_properties(agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_CACHE_ENTRIES=2" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_draw_cache_relocation_recipe_verify agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_pixel_implicit_derivatives PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 4d85df3c00..649761af8c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -503,6 +503,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe } decode = std::move(refreshed); } + if (decode != nullptr && !userPatches.empty() && verifyDrawRecipe()) { + auto patched = std::make_shared(*decode); + for (const auto& patch : userPatches) patched->programs.at(patch.program).userData.at(patch.index) = patch.value; + decode = std::move(patched); + } try { resolveDrawDecode(queue, submission, localDevice->graphicsContext(), decode, registerKey, drawKey, profile); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp index 45f1e60352..cff3638ece 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp @@ -318,6 +318,10 @@ int main(int argc, char** argv) { MovedDataHitsTheCache(libraries); UnrelocatedAliasesStayOut(libraries); MovedForwardedWordReachesStageDataHit(libraries); + if (std::getenv("APS5_VERIFY_DRAW_RECIPE") != nullptr) { + const auto counters = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(counters.verifyDecodeMismatches == 0, "the effective relocated draw decode differed from its full decode"); + } AgcDriverShutdown_nid_postfix(); std::cout << "A draw whose data moved reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; return 0; From 692d0db8f79b3a66ca43efd888ebc1200865281c Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Sun, 25 Oct 2026 23:46:11 +0200 Subject: [PATCH 092/702] test(agc): place the GCN buffer format lookup block in the guest arena On Linux libc reserves the whole guest arena at start-up, so the mmap hint at 0x1000000000 was taken and the kernel put the buffer block near the top of the address space, above the 44 bits a GCN V# carries. The hosted Linux runner failed the test there. The block now comes from the guest arena on every platform, which ends at 0xFC00000000. If it still lies above 2^44, as in an ASan build that moves the arena up, the test prints the reason and skips. The stored dwords are read back through GuestMemory::Read. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/GcnBufferFormatLookup.cpp | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp index 1254599280..e85dcfb736 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GcnBufferFormatLookup.cpp @@ -1,17 +1,11 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "Recompiler.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "VulkanTestDevice.hpp" -#ifdef _WIN32 -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#else -#include -#endif #include #include #include @@ -48,31 +42,30 @@ alignas(256) constexpr std::array PixelCode{ class LowBlock { public: - LowBlock() { -#ifdef _WIN32 - block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); -#else - void* mapped = mmap(reinterpret_cast(std::uintptr_t{1} << 36u), BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - block = mapped == MAP_FAILED ? nullptr : static_cast(mapped); -#endif - Require(block != nullptr, "GCN buffer format lookup: cannot allocate the buffer block"); - Require(reinterpret_cast(block) + BlockBytes <= GcnAddressLimit, "GCN buffer format lookup: the buffer block lies above the 44-bit GCN V# address range"); + LowBlock() : block(static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, BlockBytes))) { + Require(block != nullptr, "GCN buffer format lookup: cannot allocate the buffer block in the guest arena"); + GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, 0x04u, BlockBytes); } ~LowBlock() { -#ifdef _WIN32 - VirtualFree(block, 0, MEM_RELEASE); -#else - munmap(block, BlockBytes); -#endif + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); } LowBlock(const LowBlock&) = delete; LowBlock& operator=(const LowBlock&) = delete; + std::uintptr_t Address() const { return reinterpret_cast(block); } + bool Addressable() const { return Address() + BlockBytes <= GcnAddressLimit; } std::span Input() const { return std::span(block, 4); } std::span Output() const { return std::span(block + 64, 4); } + std::array Stored() const { + std::array words{}; + AgcDriver::GuestMemory::Read(reinterpret_cast(Output().data()), std::as_writable_bytes(std::span(words)), 4); + return words; + } + private: std::uint32_t* block = nullptr; }; @@ -163,7 +156,7 @@ void Draw(AgcDriver::VulkanDevice& device, const LowBlock& block, std::uint32_t device.WaitIdle(); const auto translated = ShaderRecompiler::GcnBufferFormats()[(dataFormat << 3u) | numberFormat]; const auto expected = translated != 0u ? InputWords[0] : 0u; - const auto output = block.Output(); + const auto output = block.Stored(); char text[96]; std::snprintf(text, sizeof(text), ": translated format %u, loaded 0x%08x, expected 0x%08x", translated, output[0], expected); Require(output[0] == expected, "GCN buffer format lookup, " + what + text); @@ -178,6 +171,10 @@ int main() { if (!device) return VulkanTestSkipped; ShaderRecompiler::SetGcnGuestShaders(true); const LowBlock block; + if (!block.Addressable()) { + std::printf("skipped, the guest arena block at 0x%llx lies above the 44-bit GCN V# address range\n", static_cast(block.Address())); + return VulkanTestSkipped; + } const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; CheckModule(*device, block, vertexMemory); const auto firstUserData = UserData(block, 4u, 4u); From 4128be2f60327e5bc90fdbcd03f05d90a6b17b7e Mon Sep 17 00:00:00 2001 From: SeaSunPT Date: Sun, 25 Oct 2026 21:50:03 +0000 Subject: [PATCH 093/702] fix(libSceAudioOut): refuse sceAudioOut2Mastering calls before sceAudioOut2Initialize The PS5 module read for #9027 returns 0x80268006 from sceAudioOut2MasteringInit, sceAudioOut2MasteringInitEx, sceAudioOut2MasteringTerm, sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam before sceAudioOut2Initialize. #9027 left the check out because AudioOut2 initialization was not tracked; AudioOut2IsInitialized_nid_no_patch now tracks it, so each of these returns 0x80268006 first, before its own mastering-state and argument checks, and changes no state. The debt entry drops the closed gap. Co-Authored-By: Claude Opus 5.5 --- .../libSceAudioOut/src/AudioOut2Mastering.cpp | 6 ++++++ .../libSceAudioOut/tests/AudioOut2Latency.cpp | 19 +++++++++++++++++++ docs/dev/debt/libSceAudioOut.md | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp index 88e948e434..514aa8f661 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp @@ -5,6 +5,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "AudioOut2Internal.hpp" namespace { @@ -12,6 +13,7 @@ constexpr int SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM = static_cast(0x80268204); constexpr int SCE_AUDIO_OUT2_ERROR_MASTERING_NOT_INITIALIZED = static_cast(0x80268203); constexpr int SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_FLAG = static_cast(0x80268205); +constexpr int SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED = static_cast(0x80268006); constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_POINTER = static_cast(0x8026800C); constexpr std::uint32_t AUDIO_OUT2_MASTERING_INIT_EX_MODE = 0x80000000u; constexpr std::uint32_t AUDIO_OUT2_MASTERING_SET_PARAM_FLAGS_MAX = 1; @@ -19,6 +21,7 @@ constexpr std::uint32_t AUDIO_OUT2_MASTERING_SET_PARAM_FLAGS_MAX = 1; std::atomic g_masteringInitialized{false}; int InitializeMastering(uint32_t flags, uint32_t accepted) { + if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED; if (g_masteringInitialized.load()) return 0; if ((flags & ~accepted) != 0) return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM; g_masteringInitialized.store(true); @@ -88,6 +91,7 @@ int APS5_VABI sceAudioOut2MasteringInitEx(uint32_t flags) { } int APS5_VABI sceAudioOut2MasteringTerm(void) { + if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED; if (!g_masteringInitialized.exchange(false)) return SCE_AUDIO_OUT2_ERROR_MASTERING_NOT_INITIALIZED; return 0; } @@ -95,6 +99,7 @@ int APS5_VABI sceAudioOut2MasteringTerm(void) { int APS5_VABI sceAudioOut2MasteringGetState(AudioOut2MasteringStatesHeader* state, uint32_t output, AudioOut2UserHandle user) { (void)output; (void)user; + if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED; if (!g_masteringInitialized.load()) return SCE_AUDIO_OUT2_ERROR_MASTERING_NOT_INITIALIZED; if (state == nullptr) return SCE_AUDIO_OUT2_ERROR_INVALID_POINTER; switch (state->states_id) { @@ -125,6 +130,7 @@ int APS5_VABI sceAudioOut2MasteringGetState(AudioOut2MasteringStatesHeader* stat int APS5_VABI sceAudioOut2MasteringSetParam(const AudioOut2MasteringParamsHeader* param, uint32_t output, uint32_t flags) { (void)output; + if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED; if (!g_masteringInitialized.load()) return SCE_AUDIO_OUT2_ERROR_MASTERING_NOT_INITIALIZED; if (param == nullptr) return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM; if (flags > AUDIO_OUT2_MASTERING_SET_PARAM_FLAGS_MAX) return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_FLAG; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index cc08e950f6..e72965d22d 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -48,6 +48,7 @@ static_assert(sizeof(AudioOut2MasteringStatesV2) == 400); constexpr int systemUser = 0xFF; constexpr int user = 0x10000000; constexpr int errorInvalidParam = static_cast(0x80268001); +constexpr int errorNotInitialized = static_cast(0x80268006); constexpr int errorInvalidPointer = static_cast(0x8026800C); constexpr int errorMasteringInvalidApiParam = static_cast(0x80268201); constexpr int errorMasteringInvalidStatesId = static_cast(0x80268204); @@ -67,6 +68,23 @@ void TestSet3DLatency() { Require(sceAudioOut2Set3DLatency(systemUser, 0xFFFFFFFFu) == errorInvalidParam, "a large output must be refused"); } +void TestMasteringBeforeInitialize() { + const AudioOut2MasteringParamsHeader params{1u}; + AudioOut2MasteringStates storage{}; + storage.states_header.states_id = 1u; + Require(sceAudioOut2MasteringInit(0) == errorNotInitialized, "mastering initialization before AudioOut2 initialization is refused"); + Require(sceAudioOut2MasteringInit(1) == errorNotInitialized, "the AudioOut2 initialization check comes before the flag check"); + Require(sceAudioOut2MasteringInitEx(0) == errorNotInitialized, "sceAudioOut2MasteringInitEx before AudioOut2 initialization is refused"); + Require(sceAudioOut2MasteringInitEx(0x80000000u) == errorNotInitialized, "sceAudioOut2MasteringInitEx with the mode bit before AudioOut2 initialization is refused"); + Require(sceAudioOut2MasteringInitEx(1) == errorNotInitialized, "the AudioOut2 initialization check comes before the sceAudioOut2MasteringInitEx flag check"); + Require(sceAudioOut2MasteringTerm() == errorNotInitialized, "termination before AudioOut2 initialization is refused"); + Require(sceAudioOut2MasteringGetState(nullptr, 0, 0) == errorNotInitialized, "the AudioOut2 initialization check comes before the null state check"); + Require(sceAudioOut2MasteringGetState(&storage.states_header, 0, 0) == errorNotInitialized, "states before AudioOut2 initialization are refused"); + Require(storage.states_header.states_id == 1u && storage.compressor_states.descriptor.id == 0u, "refused states are not written"); + Require(sceAudioOut2MasteringSetParam(¶ms, 0, 0) == errorNotInitialized, "parameters before AudioOut2 initialization are refused"); + Require(sceAudioOut2MasteringSetParam(nullptr, 0, 2) == errorNotInitialized, "the AudioOut2 initialization check comes before the parameter checks"); +} + void TestMasteringInit() { const AudioOut2MasteringParamsHeader params{1u}; AudioOut2MasteringStates storage{}; @@ -227,6 +245,7 @@ void TestLowLatencyService() { } int main() { + TestMasteringBeforeInitialize(); Require(sceAudioOut2Initialize() == 0, "initialization must succeed"); TestSet3DLatency(); TestMasteringInit(); diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index edab686a12..18dabcac73 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -13,7 +13,7 @@ - [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) - native validation requires first argument zero, a non-null buffer and exactly 1024 bytes, in that order (errors 0x80260003/04/06); the second argument is ignored. The host still reports no audio descriptors by zeroing the record. Native content comes from monitor/device services and system audio configuration, which the SDL backend does not reproduce. - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. -- [sceAudioOut2MasteringInit and sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the initialized flag and the flag checks follow the PS5 12.00 module (0x22360): a repeated initialization returns 0, other flags return `0x80268201`, and InitEx also accepts bit 31, which the console passes to the audio server as a mastering mode. Nothing is sent to an audio server, and `sceAudioOut2MasteringInit` does not return the `0x80268006` the console returns before `sceAudioOut2Initialize`, which is a missing state check of its own. PPSA01289 calls Init twice before Term twice; by the module its second Term gets `0x80268203`, and the title only logs a non-zero result +- [sceAudioOut2MasteringInit and sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the initialized flag and the flag checks follow the PS5 12.00 module (0x22360): a repeated initialization returns 0, other flags return `0x80268201`, and InitEx also accepts bit 31, which the console passes to the audio server as a mastering mode. Nothing is sent to an audio server. PPSA01289 calls Init twice before Term twice; by the module its second Term gets `0x80268203`, and the title only logs a non-zero result - [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the error `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does. The checks before them follow the PS5 12.00 module: both return `0x80268203` before mastering is initialized, GetState returns `0x8026800C` for a null state, SetParam returns `0x80268201` for a null parameter and `0x80268205` for flags above 1. The console's checks of `output` and `user` are not reproduced; both are ignored - [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. Id `0x20` with a 4-byte param throws, and the not-initialized error is not returned. Not measured on a console - [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured From 33299ee554facbcb3512416f03dc6333d3125be8 Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Wed, 21 Oct 2026 23:36:28 +0100 Subject: [PATCH 094/702] feat(libkernel_sys): implement lchflags lchflags is chflags without following a symbolic link in the last path component, as FreeBSD 11.4's sys_lchflags calls kern_chflagsat with AT_SYMLINK_NOFOLLOW. chflags now goes through a shared helper that takes the follow choice; a link in the last component is accepted as the target when not following, and the flag checks are unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 12 +++++++++--- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- core/libs/prx/libkernel_sys/Export.cpp | 6 ++++++ core/libs/prx/libkernel_sys/include/Api.hpp | 2 +- core/libs/tests/GuestKernelSys.cpp | 11 +++++++++++ docs/dev/debt/libkernel.md | 1 + 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index bf22e21d02..cb6530038a 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -710,17 +710,23 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { return 0; } -int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { +int ChangeFileFlags_nid_no_patch(const char* function, const char* path, std::uint64_t flags, bool follow) { std::string copy; if (const int error = PathError(path, copy)) return PosixFailure(error); path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); - if (const int error = LookupError(native)) return PosixFailure(error); - if (const int error = SetFileFlags("chflags", path, flags)) return PosixFailure(error); + FileStat linkStatus{}; + const bool link = !follow && native.has_filename() && File::FillLinkStat(native, &linkStatus) && (linkStatus.st_mode & 0170000) == 0120000; + if (const int error = link ? 0 : LookupError(native)) return PosixFailure(error); + if (const int error = SetFileFlags(function, path, flags)) return PosixFailure(error); return 0; } +int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { + return ChangeFileFlags_nid_no_patch("chflags", path, flags, true); +} + int APS5_VABI fchflags_nid_postfix(int d, std::uint64_t flags) { if (GuestSockets::IsOpen(d) || IsKqueueDescriptor(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); File::RefuseArchivedFile(d, "fchflags"); diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 7ec076b09f..83cf8f339b 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -239,11 +239,6 @@ int APS5_VABI ktrace_nid_postfix(void) { return 0; } -int APS5_VABI lchflags_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI mdbg_call_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 4aa5f49a80..b66421a521 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -779,6 +779,12 @@ int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int fl return 0; } +int ChangeFileFlags_nid_no_patch(const char* function, const char* path, std::uint64_t flags, bool follow); + +int APS5_VABI lchflags_nid_postfix(const char* path, std::uint64_t flags) { + return ChangeFileFlags_nid_no_patch("lchflags", path, flags, false); +} + int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { if (flag & ~GuestAtRemovedir) return static_cast(Fail(GuestEinval)); std::string copy; diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 53c0802c35..91bd474180 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -56,6 +56,7 @@ int APS5_VABI openat_nid_postfix(int fd, const char* path, int flags, ...); int APS5_VABI mkdirat_nid_postfix(int fd, const char* path, std::uint16_t mode); int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag); int APS5_VABI lchmod_nid_postfix(const char* path, std::uint16_t mode); +int APS5_VABI lchflags_nid_postfix(const char* path, std::uint64_t flags); int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid); int APS5_VABI sched_rr_get_interval_nid_postfix(int pid, KernelTimespec* interval); int APS5_VABI chown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid); @@ -115,7 +116,6 @@ int APS5_VABI ksem_trywait_nid_postfix(void); int APS5_VABI ksem_unlink_nid_postfix(void); int APS5_VABI ksem_wait_nid_postfix(void); int APS5_VABI ktrace_nid_postfix(void); -int APS5_VABI lchflags_nid_postfix(void); int APS5_VABI mdbg_call_nid_postfix(void); int APS5_VABI mdbg_service_nid_postfix(void); int APS5_VABI mount_nid_postfix(void); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index ffe0e31409..a85717cd94 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -818,6 +818,13 @@ int main() { Require(chdir_nid_postfix("/") == 0); Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && std::strcmp(directoryPath, "/") == 0); Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(lchflags_nid_postfix(file.c_str(), 0) == 0); + Require(lchflags_nid_postfix(nullptr, 0) == -1 && Error() == 14); + Require(lchflags_nid_postfix("", 0) == -1 && Error() == 2); + Require(lchflags_nid_postfix((root / "missing.bin").string().c_str(), 0) == -1 && Error() == 2); + Require(lchflags_nid_postfix((file + "/child").c_str(), 0) == -1 && Error() == 20); + Require(lchflags_nid_postfix(file.c_str(), 0x20) == -1 && Error() == 45); + Require(lchflags_nid_postfix(file.c_str(), 0x200000) == -1 && Error() == 1); const auto symbolic = (root / "symbolic").string(); const int created = symlink_nid_postfix("original.bin", symbolic.c_str()); if (created == 0) { @@ -869,6 +876,10 @@ int main() { Require(readlink_nid_postfix(dangling.c_str(), buffer, sizeof(buffer)) == 11 && !std::filesystem::exists(dangling)); Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); #endif + Require(lchflags_nid_postfix(dangling.c_str(), 0) == 0); + Require(lchflags_nid_postfix(symbolic.c_str(), 0) == 0); + Require(lchflags_nid_postfix(dangling.c_str(), 0x20) == -1 && Error() == 45); + Require(lchflags_nid_postfix(dangling.c_str(), 0x200000) == -1 && Error() == 1); } else { Require(created == -1 && Error() == 1); std::fprintf(stderr, "symbolic links unavailable: the symlink success cases not tested\n"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 875084ff1b..e7df83ff81 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -291,6 +291,7 @@ - [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled - [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. +- [lchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel_sys) has the gaps of `chflags`. As FreeBSD 11.4's [`sys_lchflags`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2357-L2363) passes `AT_SYMLINK_NOFOLLOW`, a symbolic link in the last component is the file whose flags are set instead of its target, so a dangling link is accepted. This no-follow behaviour and the console's filesystem and flag handling have not been measured on a PS5. - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `RTP_SET` throws for a real-time thread (sce attributes allow only `SCHED_FIFO`/`SCHED_RR`) and for another thread: a payload process on a PS5 (firmware 12.70) answered type 2, prio 0/15/31 on the calling thread with `EINVAL`; no accepted value or set by another thread's id measured. A non-real-time thread (POSIX attributes store `SCHED_OTHER`), unmeasured, reports 3 with 0, and `RTP_SET` accepts only that. `RTP_LOOKUP` reports the policy's type (10 or 2) and the scheduling priority, as measured, not FreeBSD's 0-31; another function fails with `ESRCH` (2 measured), a null `rtprio` with `EFAULT`, an id of no live thread with `ESRCH` (unknown ids measured). `pthread_setconcurrency` stores 0 or more, returns `EINVAL` (not errno) below 0, as libthr does; `pthread_getconcurrency` returns it, a hint. Morrowind (`mshivam019/morrowind-ps5`) imports them - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. From c7392b3fca6fb3dc36d3d53c1ff78016e4b576dc Mon Sep 17 00:00:00 2001 From: orezraey Date: Sun, 25 Oct 2026 19:21:25 -0300 Subject: [PATCH 095/702] fix(libkernel): accept EVFILT_TIMER re-adds with a time unit or on a disabled timer as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 15 ++++++--------- core/libs/tests/GuestKqueue.cpp | 16 ++++++++++++++-- docs/dev/debt/libkernel.md | 4 ++-- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index cbddf855ce..dc11572405 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -363,20 +363,20 @@ int ModifyTimerKnote(const KernelEqueueRef& queue, KernelEvent& change) { if ((change.flags & ~(EV_ADD | EV_ONESHOT)) != 0) { NotImplemented_nid_no_patch("kevent: re-adding an EVFILT_TIMER with flags other than EV_ONESHOT"); } - if (change.data <= 0 || change.fflags != 0) { - NotImplemented_nid_no_patch("kevent: re-adding an EVFILT_TIMER with nonzero fflags or a period of 0 or less"); - } const auto periodNs = TimerPeriodNanos(change.data, change.fflags); + if (change.data <= 0 || (change.fflags & ~NOTE_TIMER_PRECMASK) != 0 || !periodNs) { + NotImplemented_nid_no_patch("kevent: re-adding an EVFILT_TIMER with a period of 0 or less or an unknown unit"); + } bool oneShot = false; - bool disabled = false; bool overflow = false; - const int error = GuestErrno(queue->EditEvent(change.ident, change.filter, [&change, &oneShot, &disabled, &overflow, periodNs](KernelEqueueEvent& event) { + const int error = GuestErrno(queue->EditEvent(change.ident, change.filter, [&change, &oneShot, &overflow, periodNs](KernelEqueueEvent& event) { if (event.intervalNs == 0) { oneShot = true; return; } if (!event.enabled) { - disabled = true; + event.intervalNs = *periodNs; + event.event.udata = change.udata; return; } if (*periodNs > std::numeric_limits::max() - event.deadlineNs) { @@ -389,9 +389,6 @@ int ModifyTimerKnote(const KernelEqueueRef& queue, KernelEvent& change) { if (oneShot) { NotImplemented_nid_no_patch("kevent: re-adding a one-shot EVFILT_TIMER"); } - if (disabled) { - NotImplemented_nid_no_patch("kevent: re-adding a disabled EVFILT_TIMER"); - } if (overflow) { NotImplemented_nid_no_patch("kevent: re-adding an EVFILT_TIMER with a period that overflows the armed expiry"); } diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 12afea42a1..b2e783d851 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -61,6 +61,7 @@ static constexpr std::uint32_t NoteTrigger = 0x01000000; static constexpr std::uint32_t NoteLowat = 0x0001; static constexpr std::uint32_t NoteFfcopy = 0xc0000000; static constexpr std::uint32_t NoteFfor = 0x80000000; +static constexpr std::uint32_t NoteMseconds = 0x0002; static constexpr std::uint32_t NoteUseconds = 0x0004; static constexpr std::uint32_t NoteNseconds = 0x0008; static constexpr std::uint32_t NoteDelete = 0x0001; @@ -215,13 +216,24 @@ static void TimerReadd() { Require(Apply(kq, Change(10, EvfiltTimer, EvAdd | EvOneshot, 0, 100000)) == 0); Require(Throws([kq] { Apply(kq, Change(10, EvfiltTimer, EvAdd, 0, 5)); })); Require(Apply(kq, Change(11, EvfiltTimer, EvAdd, 0, 100000)) == 0); - Require(Throws([kq] { Apply(kq, Change(11, EvfiltTimer, EvAdd, NoteUseconds, 5)); })); + Require(Apply(kq, Change(11, EvfiltTimer, EvAdd, NoteMseconds, 100000)) == 0); + Require(Throws([kq] { Apply(kq, Change(11, EvfiltTimer, EvAdd, NoteMseconds | NoteUseconds, 5)); })); + Require(Throws([kq] { Apply(kq, Change(11, EvfiltTimer, EvAdd, 0x10, 5)); })); Require(Throws([kq] { Apply(kq, Change(11, EvfiltTimer, EvAdd, 0, 0)); })); Require(Throws([kq] { Apply(kq, Change(11, EvfiltTimer, EvAdd, 0, INTPTR_MAX)); })); Require(Throws([kq] { Apply(kq, Change(11, EvfiltTimer, EvAdd | EvClear, 0, 5)); })); Require(Throws([kq] { Apply(kq, Change(11, EvfiltTimer, EvEnable, 0, 5)); })); Require(Apply(kq, Change(12, EvfiltTimer, EvAdd | EvDisable, 0, 100000)) == 0); - Require(Throws([kq] { Apply(kq, Change(12, EvfiltTimer, EvAdd, 0, 5)); })); + Require(Apply(kq, Change(12, EvfiltTimer, EvAdd, 0, 5)) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(60)); + Require(Poll(kq, events, 4) == 0); + + Require(Apply(kq, Change(13, EvfiltTimer, EvAdd, 0, 30)) == 0); + const auto unitArmed = std::chrono::steady_clock::now(); + Require(Apply(kq, Change(13, EvfiltTimer, EvAdd, NoteMseconds, 30)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == 13); + Require(std::chrono::steady_clock::now() - unitArmed >= std::chrono::milliseconds(20)); + Require(Apply(kq, Change(13, EvfiltTimer, EvDelete)) == 0); Require(close_nid_postfix(kq) == 0); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 875084ff1b..f32aeee833 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -143,7 +143,7 @@ - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_open, sem_close, sem_unlink](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) keep named semaphores in a table inside the process; FreeBSD 11.4's [`sem_new.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sem_new.c#L138-L308) backs each with a file `/tmp/SEMD`. A semaphore lasts until `sem_unlink` and its last `sem_close` but is not shared with other processes, and the mode is ignored (no permission errors, as for a root caller). Whether a title can create files under `/tmp` on the console is unverified. A name with a further `/` throws, as it would resolve below `/tmp`, and so does an initial value above `SEM_VALUE_MAX`, which FreeBSD stores unchecked. - [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. -- [kevent measurements](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - in payload processes on a PS5 (firmware 12.70), re-adding an existing periodic `EVFILT_TIMER` with `EV_ADD`, `fflags` 0 and a positive period kept the armed expiry and the expirations already counted and applied the new period after that expiry, as FreeBSD 11.0 does, where 11.4's `filt_timertouch` restarts the timer: a 50 ms timer re-added with 10 ms fired first 46 ms after the re-add and then every 10 ms, a 10 ms timer re-added with 100 ms fired first 5 ms after the re-add and then every 100 ms, the 12 expirations counted before a re-add were reported after it, and `EV_ONESHOT` in the re-add left the timer periodic. Here such a re-add does the same; re-adding a one-shot or disabled timer, a re-add with nonzero `fflags` (`NOTE_MSECONDS` included), a period of 0 or less, a period too large to add to the armed expiry or flags other than `EV_ONESHOT` were not measured and throw here, and the other changes to an existing timer except `EV_DELETE` throw here too. There `EV_ADD` failed with `EPERM` for `EVFILT_AIO`, `EVFILT_LIO` and `EVFILT_SENDFILE` at every ident tried (0, 5, 999, 2^40 and an open pipe) and, at ident 5, also with `EV_CLEAR`, `EV_ONESHOT` or `EV_DISABLE` added, and `EV_DELETE` and `EV_ENABLE` without `EV_ADD` failed with `ENOENT`. `EVFILT_PROCDESC` failed with `EBADF` on a descriptor that is not open and on an ident above `INT_MAX`, and with `EINVAL` on every open descriptor tried: Unix-domain stream and datagram sockets, descriptor 0 (a socket), an `AF_INET` UDP socket, both ends of a pipe, a regular file, a directory, another kqueue and the kqueue itself; `EV_DELETE` on a socket failed with `ENOENT`. Here these four filters answer the same way. `EV_ADD` of `EVFILT_PROCDESC` fails with `EINVAL` on the kinds of descriptor tried, since there are no process descriptors here, and also on shared memory and JIT shared memory, which are host files here: FreeBSD 11.0 gives `shm_open` objects [`invfo_kqfilter`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_shm.c#L136), which answers `EINVAL`, but neither was measured. It throws on any other kind, such as `/dev/random` and `/dev/urandom`, and on descriptors 0 to 2 that are not sockets or kqueues, whatever they hold: the host's standard streams, a pipe or file that `dup2` put on one of them, or a descriptor opened on a number that was free when libkernel loaded. A change of any of the four without `EV_ADD` fails with `ENOENT`, for `EVFILT_PROCDESC` after the descriptor check, as `kqueue_register` finds no knote. When `EV_ADD` of one of the four fails with `EPERM`, or with `EINVAL` on a descriptor other than the kqueue itself, the error event has `fflags` 0 and the errno in `data`, as FreeBSD 11.0's [`kqueue_register`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1302-L1316) clears `fflags` and `data` before the attach and [`kqueue_kevent`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005-L1006) then stores the error in `data`; a descriptor that is not open, the kqueue itself and `EV_ENABLE` with `EV_DISABLE` fail before that and keep their `fflags`. That error event was not measured. `EVFILT_PROC` on the process itself with `NOTE_EXIT`, `EVFILT_SIGNAL` for `SIGUSR1` and `EVFILT_FS` at ident 0 were accepted there and still throw here. Also measured there on Unix-domain socketpairs, and still throwing here: a `NOTE_LOWAT` mark of 0 on an empty stream socket gave a read event at once (`data` 0), and so did re-adding that knote with a mark of -1 once the socket was drained; a write mark of 2 on the other end of that stream socketpair gave an event (`data` 8192), and re-adding that knote with 2^30 gave none; a mark of 5 on a datagram socket gave no read event after one 1-byte datagram and a read event (`data` 11) after a second, 10-byte datagram. `EVFILT_READ` on another kqueue reported the number of events pending in it there (1, then 2) and throws here; `EVFILT_WRITE` on a kqueue and a kqueue registered on itself failed with `EINVAL` there, as they do here +- [kevent measurements](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - in payload processes on a PS5 (firmware 12.70), re-adding an existing periodic `EVFILT_TIMER` with `EV_ADD`, `fflags` 0 and a positive period kept the armed expiry and the expirations already counted and applied the new period after that expiry, as FreeBSD 11.0 does, where 11.4's `filt_timertouch` restarts the timer: a 50 ms timer re-added with 10 ms fired first 46 ms after the re-add and then every 10 ms, a 10 ms timer re-added with 100 ms fired first 5 ms after the re-add and then every 100 ms, the 12 expirations counted before a re-add were reported after it, and `EV_ONESHOT` in the re-add left the timer periodic. Here such a re-add does the same; on firmware 13.60 a re-add with a unit in `fflags` (`NOTE_MSECONDS`, `NOTE_NSECONDS`) also applied the new period after the armed expiry, and re-adding a disabled timer returned 0 and left it disabled, as here; re-adding an armed one-shot timer, a period of 0 or less, a period too large to add to the armed expiry, other `fflags` or flags other than `EV_ONESHOT` were not measured and throw here, and the other changes to an existing timer except `EV_DELETE` throw here too. There `EV_ADD` failed with `EPERM` for `EVFILT_AIO`, `EVFILT_LIO` and `EVFILT_SENDFILE` at every ident tried (0, 5, 999, 2^40 and an open pipe) and, at ident 5, also with `EV_CLEAR`, `EV_ONESHOT` or `EV_DISABLE` added, and `EV_DELETE` and `EV_ENABLE` without `EV_ADD` failed with `ENOENT`. `EVFILT_PROCDESC` failed with `EBADF` on a descriptor that is not open and on an ident above `INT_MAX`, and with `EINVAL` on every open descriptor tried: Unix-domain stream and datagram sockets, descriptor 0 (a socket), an `AF_INET` UDP socket, both ends of a pipe, a regular file, a directory, another kqueue and the kqueue itself; `EV_DELETE` on a socket failed with `ENOENT`. Here these four filters answer the same way. `EV_ADD` of `EVFILT_PROCDESC` fails with `EINVAL` on the kinds of descriptor tried, since there are no process descriptors here, and also on shared memory and JIT shared memory, which are host files here: FreeBSD 11.0 gives `shm_open` objects [`invfo_kqfilter`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_shm.c#L136), which answers `EINVAL`, but neither was measured. It throws on any other kind, such as `/dev/random` and `/dev/urandom`, and on descriptors 0 to 2 that are not sockets or kqueues, whatever they hold: the host's standard streams, a pipe or file that `dup2` put on one of them, or a descriptor opened on a number that was free when libkernel loaded. A change of any of the four without `EV_ADD` fails with `ENOENT`, for `EVFILT_PROCDESC` after the descriptor check, as `kqueue_register` finds no knote. When `EV_ADD` of one of the four fails with `EPERM`, or with `EINVAL` on a descriptor other than the kqueue itself, the error event has `fflags` 0 and the errno in `data`, as FreeBSD 11.0's [`kqueue_register`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1302-L1316) clears `fflags` and `data` before the attach and [`kqueue_kevent`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005-L1006) then stores the error in `data`; a descriptor that is not open, the kqueue itself and `EV_ENABLE` with `EV_DISABLE` fail before that and keep their `fflags`. That error event was not measured. `EVFILT_PROC` on the process itself with `NOTE_EXIT`, `EVFILT_SIGNAL` for `SIGUSR1` and `EVFILT_FS` at ident 0 were accepted there and still throw here. Also measured there on Unix-domain socketpairs, and still throwing here: a `NOTE_LOWAT` mark of 0 on an empty stream socket gave a read event at once (`data` 0), and so did re-adding that knote with a mark of -1 once the socket was drained; a write mark of 2 on the other end of that stream socketpair gave an event (`data` 8192), and re-adding that knote with 2^30 gave none; a mark of 5 on a datagram socket gave no read event after one 1-byte datagram and a read event (`data` 11) after a second, 10-byte datagram. `EVFILT_READ` on another kqueue reported the number of events pending in it there (1, then 2) and throws here; `EVFILT_WRITE` on a kqueue and a kqueue registered on itself failed with `EINVAL` there, as they do here - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` 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`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond. Birth-time clamping is not atomic with concurrent host timestamp updates and has not been measured on a console - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory @@ -211,7 +211,7 @@ - [sceKernelGetSystemExVersion, sysKernelGetLowerLimitSysexVersion, sysKernelGetLowerLimitUpdVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) and [sceKernelGetTraceMemoryStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - follow libkernel.sprx, libkernel_sys.sprx and libkernel_web.sprx of firmware 13.42, read from disassembly and not measured on a console. None of them asks the kernel: the first writes an empty string to both outputs and returns a plain 22, not `SCE_KERNEL_ERROR_EINVAL`, when either is null; the lower limits write an empty string and a 32-bit 0; the trace memory statistics write 0 to each output that is not null. The meaning of the outputs is not known - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less - [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; a payload process on a PS5 (firmware 12.70) reads 0x800000000 for both limits. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors, which guest files, pipes and sockets all take, and as many stdio streams, which libkernel raises from 512 at load with [`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio), on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load, and on macOS the smaller of the host's hard limit and `kern.maxfilesperproc`, which libkernel sets as both limits at load, since macOS fails with `EMFILE` at `kern.maxfilesperproc` whatever the soft limit. A payload process on a PS5 (firmware 12.70) reads 13952 for both limits, while `getdtablesize` returns 12555 and the process can open descriptors up to 12554. XPSemu (PPSA97358) reads `RLIMIT_NOFILE` in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK` (0x200000), `RLIMIT_RSS` and `RLIMIT_MEMLOCK` (infinity) and `RLIMIT_NPROC` (1604) report the soft and hard limits a payload process reads on a PS5 (firmware 12.70). Guest locking still follows the host: `mlock` and `sceKernelMlock` fail at the host's hard `RLIMIT_MEMLOCK` on Linux, and on Windows when the process working set cannot grow, although `RLIMIT_MEMLOCK` reports infinity. There the main thread's stack is 0x200000 as well (`pthread_attr_get_np`); here the main thread runs on the host thread's stack, whose size can differ. Whether a title process reads the same limits is unverified. -- [EVFILT_TIMER modify with data 0](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - a timer change with `EV_ADD`, `data` and `fflags` unset keeps the note's period instead of taking FreeBSD's zero default, which would fire without delay; whether the console keeps or zeroes the period is unverified. `EV_ENABLE` on a disabled timer reports the expiries it missed while disabled, as FreeBSD 11's `filt_timerexpire` counts a disabled note and keeps its phase: the missed count is summed in one step at the first wait and the next expiry keeps the note's phase, which `TimerDisableEnableRearm` pins; not measured on a console. A change that sets `EV_ADD`, `data` or `fflags` still throws, waiting for the measured re-add rules: FreeBSD 11's `kqueue_register` copies `data` and `fflags` onto the note and the new period applies at the next expiry. +- [EVFILT_TIMER modify with data 0](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - a timer change with `EV_ADD`, `data` and `fflags` unset keeps the note's period, where a payload process on PS5 firmware 13.60 took FreeBSD's zero default: the timer fired at its armed expiry and then about every millisecond, reporting 10 to 13 expiries each time. `EV_ENABLE` on a disabled timer reports the expiries it missed while disabled, as FreeBSD 11's `filt_timerexpire` counts a disabled note and keeps its phase: the missed count is summed in one step at the first wait and the next expiry keeps the note's phase, which `TimerDisableEnableRearm` pins; on that console `EV_DISABLE` and `EV_ENABLE` with `data` 0 zeroed the period too, so the enable reported about 912 expiries after 120 ms and the timer then fired about every millisecond. A change that sets `EV_ADD`, `data` or `fflags` still throws, waiting for the measured re-add rules: FreeBSD 11's `kqueue_register` copies `data` and `fflags` onto the note and the new period applies at the next expiry. - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443. `RLIMIT_NPROC` follows FreeBSD 11 by clamping both values to 1 through `maxprocperuid`, stores the result and returns it from `getrlimit`. The initial 1604 and `maxprocperuid` 1443 follow that measurement: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` applies that bound ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)). `kern.maxproc` fails with `ENOENT` in the payload process, so the values cannot be queried independently, and process creation does not enforce the stored limit. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) turns off Winsock's report of an ICMP port-unreachable reply on datagram sockets (`SIO_UDP_CONNRESET`). Winsock fails the next receive on any datagram socket with `WSAECONNRESET`, where FreeBSD's [`udp_common_ctlinput`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/netinet/udp_usrreq.c#L774-L798) reports the reply only to a socket connected to the sender. So on Windows a datagram socket connected to a port nobody listens on does not fail its next send or receive with `ECONNREFUSED` either - [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. From db79e3cd0257d07035d2c27f8f6cade981e6b904 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:37:51 -0400 Subject: [PATCH 096/702] feat(libSceRudp): implement sceRudpPollCancel as firmware 13.42's library does A cancel with no waiter is kept once for that poll and makes its next sceRudpPollWait return 0x80770014, also with a zero timeout. A cancel with waiters wakes all of them with 0x80770014 and returns once none is left. Behaviour from @BunnyFl's execution of the 13.42 libSceRudp.sprx in #10954. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceRudp/Export.cpp | 68 ++++++++++++++++++++---- core/libs/prx/libSceRudp/include/Api.hpp | 2 +- core/libs/tests/GuestRudpPoll.cpp | 32 +++++++++++ docs/dev/debt/libSceRudp.md | 3 +- 4 files changed, 94 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index dd4dd2c260..e24efe6cf2 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -25,6 +25,7 @@ constexpr int RUDP_ERROR_MEMORY = static_cast(0x80770007u); constexpr int RUDP_ERROR_CONN_RESET = static_cast(0x80770009u); constexpr int RUDP_ERROR_CONN_REFUSED = static_cast(0x8077000Au); constexpr int RUDP_ERROR_NOT_ACCEPTABLE = static_cast(0x80770011u); +constexpr int RUDP_ERROR_CANCELLED = static_cast(0x80770014u); constexpr int RUDP_ERROR_INVALID_POLL_ID = static_cast(0x8077001Fu); constexpr int RUDP_ERROR_TOO_MANY_CONTEXTS = static_cast(0x80770020u); @@ -89,6 +90,9 @@ void* g_handler_arg = nullptr; struct RudpPoll { std::size_t capacity; std::map entries; + int waiters = 0; + uint64_t cancels = 0; + bool cancelPending = false; }; int g_next_poll = 1; @@ -443,16 +447,39 @@ int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, std::size_t eve if (pollId < 0 || events == nullptr || eventLen == 0) { return RUDP_ERROR_INVALID_ARGUMENT; } - if (g_polls.count(pollId) == 0) { + const auto poll = g_polls.find(pollId); + if (poll == g_polls.end()) { return RUDP_ERROR_INVALID_POLL_ID; } - const auto closed = [pollId] { return !g_inited || g_polls.count(pollId) == 0; }; - if (timeout > RUDP_POLL_WAIT_MAX_USEC) { - g_polls_changed.Wait(lk, closed); - } else if (!g_polls_changed.WaitUntil(lk, TimedWait::DeadlineNanos(timeout), closed)) { - return 0; + if (poll->second.cancelPending) { + poll->second.cancelPending = false; + return RUDP_ERROR_CANCELLED; } - return g_inited ? RUDP_ERROR_INVALID_POLL_ID : RUDP_ERROR_NOT_INITIALIZED; + const uint64_t cancels = poll->second.cancels; + ++poll->second.waiters; + const auto woken = [pollId, cancels] { + if (!g_inited) { + return true; + } + const auto current = g_polls.find(pollId); + return current == g_polls.end() || current->second.cancels != cancels; + }; + bool signalled = true; + if (timeout > RUDP_POLL_WAIT_MAX_USEC) { + g_polls_changed.Wait(lk, woken); + } else { + signalled = g_polls_changed.WaitUntil(lk, TimedWait::DeadlineNanos(timeout), woken); + } + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + const auto current = g_polls.find(pollId); + if (current == g_polls.end()) { + return RUDP_ERROR_INVALID_POLL_ID; + } + --current->second.waiters; + g_polls_changed.NotifyAll(); + return signalled ? RUDP_ERROR_CANCELLED : 0; } int APS5_VABI sceRudpPollDestroy(int pollId) { @@ -490,8 +517,31 @@ int APS5_VABI sceRudpListen() { return 0; } -int APS5_VABI sceRudpPollCancel() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpPollCancel(int pollId) { + std::unique_lock lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + const auto poll = g_polls.find(pollId); + if (poll == g_polls.end()) { + return RUDP_ERROR_INVALID_POLL_ID; + } + if (poll->second.waiters == 0) { + poll->second.cancelPending = true; + return 0; + } + ++poll->second.cancels; + g_polls_changed.NotifyAll(); + g_polls_changed.Wait(lk, [pollId] { + if (!g_inited) { + return true; + } + const auto current = g_polls.find(pollId); + return current == g_polls.end() || current->second.waiters == 0; + }); return 0; } diff --git a/core/libs/prx/libSceRudp/include/Api.hpp b/core/libs/prx/libSceRudp/include/Api.hpp index 44b6881d58..72a945262b 100644 --- a/core/libs/prx/libSceRudp/include/Api.hpp +++ b/core/libs/prx/libSceRudp/include/Api.hpp @@ -38,7 +38,7 @@ int APS5_VABI sceRudpAccept(); int APS5_VABI sceRudpFlush(); int APS5_VABI sceRudpGetLocalInfo(); int APS5_VABI sceRudpListen(); -int APS5_VABI sceRudpPollCancel(); +int APS5_VABI sceRudpPollCancel(int pollId); int APS5_VABI sceRudpSetMaxSegmentSize(); } diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp index 21274887b3..8ae670e24e 100644 --- a/core/libs/tests/GuestRudpPoll.cpp +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -25,6 +25,7 @@ int main() { constexpr int notInitialized = static_cast(0x80770001u); constexpr int invalidContext = static_cast(0x80770003u); constexpr int invalidArgument = static_cast(0x80770004u); + constexpr int cancelled = static_cast(0x80770014u); constexpr int invalidPoll = static_cast(0x8077001Fu); constexpr int tooManyContexts = static_cast(0x80770020u); constexpr int add = 1; @@ -41,6 +42,7 @@ int main() { Require(sceRudpPollControl(1, add, 0, read) == notInitialized); Require(sceRudpPollWait(1, events.data(), eventCount, 0) == notInitialized); Require(sceRudpPollDestroy(1) == notInitialized); + Require(sceRudpPollCancel(0) == notInitialized); Require(sceRudpInit_nid_postfix(pool.data(), static_cast(pool.size())) == 0); Require(sceRudpPollCreate(0) == invalidArgument); @@ -108,6 +110,35 @@ int main() { Require(sceRudpPollControl(poll, add, third, read) == 0); Require(sceRudpPollControl(other, add, third, read) == 0); + Require(sceRudpPollCancel(-1) == invalidArgument); + Require(sceRudpPollCancel(std::numeric_limits::min()) == invalidArgument); + Require(sceRudpPollCancel(12345) == invalidPoll); + Require(sceRudpPollCancel(poll) == 0); + Require(sceRudpPollCancel(poll) == 0); + Require(sceRudpPollWait(other, events.data(), eventCount, 1000) == 0); + Require(sceRudpPollWait(poll, events.data(), eventCount, 1000) == cancelled); + Require(sceRudpPollWait(poll, events.data(), eventCount, 1000) == 0); + Require(sceRudpPollCancel(poll) == 0); + Require(sceRudpPollWait(poll, events.data(), eventCount, 0) == cancelled); + Require(sceRudpPollWait(poll, events.data(), eventCount, 0) == 0); + + const int cancelPoll = sceRudpPollCreate(1); + Require(cancelPoll > 0); + int foreverResult = 0; + int timedResult = 0; + std::thread foreverWaiter([&] { foreverResult = sceRudpPollWait(cancelPoll, events.data(), eventCount, forever); }); + std::thread timedWaiter([&] { timedResult = sceRudpPollWait(cancelPoll, events.data(), eventCount, 10000000); }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const auto cancelledAt = std::chrono::steady_clock::now(); + Require(sceRudpPollCancel(cancelPoll) == 0); + Require(std::chrono::steady_clock::now() - cancelledAt < std::chrono::milliseconds(500)); + foreverWaiter.join(); + timedWaiter.join(); + Require(foreverResult == cancelled && timedResult == cancelled); + Require(sceRudpPollWait(cancelPoll, events.data(), eventCount, 1000) == 0); + Require(sceRudpPollDestroy(cancelPoll) == 0); + Require(sceRudpPollCancel(cancelPoll) == invalidPoll); + const int finitePoll = sceRudpPollCreate(1); Require(finitePoll > 0); int finiteResult = 0; @@ -137,6 +168,7 @@ int main() { Require(sceRudpEnd() == 0); endedWaiter.join(); Require(endedResult == notInitialized); + Require(sceRudpPollCancel(other) == notInitialized); Require(sceRudpInit_nid_postfix(pool.data(), static_cast(pool.size())) == 0); Require(sceRudpPollDestroy(other) == invalidPoll); diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index 6b667bd48d..48736eb03a 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -12,4 +12,5 @@ - [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. - [sceRudpGetOption, sceRudpSetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - full PS5 option contracts are unverified; defaults and validation follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). Options 12 and 13 have unknown meanings; last-error option 14 is not maintained. Their setters store all 32 bits per context and return 0 for valid four-byte buffers, preserving acceptance on `main`; their getters throw. Static inspection of PPSA06888 v01.001.008's bundled Unity transport shows its context-set constructor configuring IDs 2, 3, 5 and 6; runtime usage and other paths remain unverified. The plugin wrappers pass four-byte values. No context can connect, so stored settings cannot affect traffic. Closes when: PS5 option contracts are verified and the remaining behavior is implemented. - [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5; imported by PPSA31327 and not called before the main menu. Signatures and the 8-byte event layout are recovered from the supplied module. The operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp), and the error codes match [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented, so `int sceRudpPollDestroy(int pollId)` and its checks are inferred. No context can connect, so no event is ever reported: `sceRudpPollWait` returns 0 when its timeout elapses, also for an empty poll, where Vita3K returns at once -- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen, sceRudpPollCancel and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceRudpPollCancel](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5. `int sceRudpPollCancel(int pollId)` and its behaviour follow @BunnyFl's run of firmware 13.42's `libSceRudp.sprx` in [#10954](https://github.com/boykopovar/AnyPS5/pull/10954), which executed the library with its kernel and libc imports stubbed; it is not a console measurement. Before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED`, for a negative id `RUDP_ERROR_INVALID_ARGUMENT` and for an unknown id `RUDP_ERROR_INVALID_POLL_ID`. With no waiter, one cancellation is kept for that poll however often it is cancelled, and the next `sceRudpPollWait` on it, also with a zero timeout, returns `0x80770014` at once. With waiters, all of them return `0x80770014` and the call returns 0 once none is left; nothing stays pending. How a cancellation races a timeout is unverified. +- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From 14b096d3d68406aeccc7b56f510f0d8616447cb1 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:40:10 +0000 Subject: [PATCH 097/702] test(libc): stop pinning unsupported prefix inputs --- core/libs/prx/libc/include/Api.hpp | 2 ++ core/libs/prx/libc/src/FloatingPrefix.cpp | 10 +--------- core/libs/tests/GuestFloatingPrefix.cpp | 18 ------------------ 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index b1e66a54b6..72c1d1e9d6 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -975,6 +975,8 @@ long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, i long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base); unsigned long long APS5_VABI wcstoumax_nid_postfix(const char16_t* str, char16_t** endptr, int base); std::uint64_t APS5_VABI _WStoul_nid_postfix(const char16_t* str, char16_t** endptr, int base); +int APS5_VABI _Stopfx_nid_postfix(const char** cursor, char** end); +int APS5_VABI _WStopfx_nid_postfix(const char16_t** cursor, char16_t** end); int APS5_VABI wcscoll_nid_postfix(const char16_t* first, const char16_t* second); size_t APS5_VABI wcsxfrm_nid_postfix(char16_t* destination, const char16_t* source, size_t count); size_t APS5_VABI strxfrm_nid_postfix(char* destination, const char* source, size_t count); diff --git a/core/libs/prx/libc/src/FloatingPrefix.cpp b/core/libs/prx/libc/src/FloatingPrefix.cpp index 1e399e8514..bd85e4ed16 100644 --- a/core/libs/prx/libc/src/FloatingPrefix.cpp +++ b/core/libs/prx/libc/src/FloatingPrefix.cpp @@ -1,13 +1,5 @@ #include "prx/libc/include/General.hpp" - -extern "C" { -int APS5_VABI isspace_nid_postfix(int); -int APS5_VABI isalnum_nid_postfix(int); -int APS5_VABI isxdigit_nid_postfix(int); -int APS5_VABI iswspace_nid_postfix(std::uint32_t); -int APS5_VABI iswalnum_nid_postfix(std::uint32_t); -int APS5_VABI iswxdigit_nid_postfix(std::uint32_t); -} +#include "prx/libc/include/Api.hpp" namespace { diff --git a/core/libs/tests/GuestFloatingPrefix.cpp b/core/libs/tests/GuestFloatingPrefix.cpp index 2fbadc6be7..cf81250bad 100644 --- a/core/libs/tests/GuestFloatingPrefix.cpp +++ b/core/libs/tests/GuestFloatingPrefix.cpp @@ -88,22 +88,6 @@ static void CheckOne(Prefix function, const TChar* text, const Case& test Require(function(&cursor, nullptr) == test.code && cursor == text + test.cursor, "optional end pointer"); } -template -static void CheckUnsupported(Prefix function) { - TChar sentinel{}; - TChar* end = &sentinel; - const TChar* cursor = nullptr; - for (const bool nullSlot : {false, true}) { - bool threw = false; - try { - function(nullSlot ? nullptr : &cursor, &end); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw && cursor == nullptr && end == &sentinel, "null input must remain explicitly unsupported"); - } -} - template static void Check(Prefix function, unsigned payloadLimit = 256) { for (const auto& test : Cases) { @@ -211,12 +195,10 @@ int main(int argc, char** argv) { if (wide) { const auto prefix = reinterpret_cast>(function); Check(prefix); - CheckUnsupported(prefix); CheckConcurrent(prefix); } else { const auto prefix = reinterpret_cast>(function); Check(prefix); - CheckUnsupported(prefix); CheckConcurrent(prefix); } std::printf("%s: prefix codes, pointer updates, input/output bounds and guarded reads passed\n", name.c_str()); From 5c134a8feee0910a64726623e0cfcac06beffac5 Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Mon, 26 Oct 2026 05:19:42 +0530 Subject: [PATCH 098/702] feat(macos): start relinked games A relinked macOS game stops in dyld before any guest code runs: libc does not export the guest entry and TLS functions the macOS relinker's executable imports, and AppKit only allows windows on the process main thread. - libc: add Aps5StartGuest, which the relinker's entry stub calls with the guest entry and the FreeBSD initial block. It runs the guest on a second thread and keeps the main thread serving the main dispatch queue. - libc: add the guest TLS functions the relinked code imports (Aps5GuestTlsRegister, Aps5GuestTlsAllocateBlock, Aps5GuestTlsGetAddr, Aps5GuestTlsUseXsave). - libc: MainThread.hpp's RunOnMainThread runs a call on the main thread, waits for it and rethrows its exception. It runs on the calling thread when the main thread does not serve the main queue, as in the tests. - libSceVideoOut: create, resize, retitle and destroy the window, initialise SDL video, create the Vulkan surface and pump events through RunOnMainThread on macOS. - libc: find the process parameters through dl_iterate_phdr on macOS too, and link CoreFoundation. - relinker: leave 0x400 bytes after the Mach-O load commands, like ld's -headerpad, so codesign's LC_CODE_SIGNATURE no longer overwrites the entry stub. Linux and Windows compile the same libc and libSceVideoOut code as before. --- .../prx/libSceVideoOut/src/DisplayWindow.cpp | 22 +++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 20 +++ core/libs/prx/libc/CMakeLists.txt | 6 + core/libs/prx/libc/include/MainThread.hpp | 37 ++++ .../libc/src/ApplicationProcessParameters.cpp | 4 +- .../libc/src/specifics/macos/GuestStart.cpp | 75 ++++++++ .../prx/libc/src/specifics/macos/GuestTls.cpp | 161 ++++++++++++++++++ .../elfpatcher/src/macos/MacOsImage.cpp | 3 +- 8 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libc/include/MainThread.hpp create mode 100644 core/libs/prx/libc/src/specifics/macos/GuestStart.cpp create mode 100644 core/libs/prx/libc/src/specifics/macos/GuestTls.cpp diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index 35c6c86917..30fdf21f22 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -15,6 +15,10 @@ #include "SDL_syswm.h" #include #include +#endif +#ifdef __APPLE__ +#include "prx/libc/include/MainThread.hpp" + #endif namespace { @@ -51,6 +55,9 @@ void DisplayWindow::Ensure(std::uint32_t sourceWidth, std::uint32_t sourceHeight } void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { +#ifdef __APPLE__ + RunOnMainThread([&] { +#endif SDL_Rect usable{}; require(SDL_GetDisplayUsableBounds(0, &usable) == 0, SDL_GetError()); require(DisplayWindowInitialSizePercent > 0 && DisplayWindowInitialSizePercent <= 100, "initial window size percent must be between 1 and 100"); @@ -67,6 +74,9 @@ void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight SDL_SetWindowMinimumSize(window, static_cast(DisplayWindowMinimumWidth), static_cast(DisplayWindowMinimumHeight)); applyAppIcon(window); installSubclass(); +#ifdef __APPLE__ + }); +#endif } void DisplayWindow::updateAspectRatio(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { @@ -77,7 +87,11 @@ void DisplayWindow::updateAspectRatio(std::uint32_t sourceWidth, std::uint32_t s void DisplayWindow::Destroy() noexcept { if (window == nullptr) return; removeSubclass(); +#ifdef __APPLE__ + RunOnMainThread([&] { SDL_DestroyWindow(window); }); +#else SDL_DestroyWindow(window); +#endif window = nullptr; } @@ -88,7 +102,11 @@ SDL_Window* DisplayWindow::Handle() const { void DisplayWindow::ToggleFullscreen() { require(window != nullptr, "window must exist before toggling fullscreen"); const auto flags = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0 ? 0u : static_cast(SDL_WINDOW_FULLSCREEN_DESKTOP); +#ifdef __APPLE__ + RunOnMainThread([&] { require(SDL_SetWindowFullscreen(window, flags) == 0, SDL_GetError()); }); +#else require(SDL_SetWindowFullscreen(window, flags) == 0, SDL_GetError()); +#endif } void DisplayWindow::DrawableSize(std::uint32_t& width, std::uint32_t& height) const { @@ -121,7 +139,11 @@ void DisplayWindow::UpdateTitle() { } char text[160]; std::snprintf(text, sizeof(text), "%s | FPS: %.2f (%llu)", title.value, currentFps, static_cast(frameNum)); +#ifdef __APPLE__ + RunOnMainThread([&] { SDL_SetWindowTitle(window, text); }); +#else SDL_SetWindowTitle(window, text); +#endif if (auto* telemetry = VideoOutTelemetry::Instance(); telemetry != nullptr && (frameNum == 1 || frameNum % 30 == 0)) telemetry->EmitPresent(frameNum, currentFps); } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 377fb3548b..b97cdafcf4 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -33,6 +33,9 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/Shutdown.hpp" +#ifdef __APPLE__ +#include "prx/libc/include/MainThread.hpp" +#endif namespace { @@ -74,7 +77,13 @@ std::vector windowExtensions(SDL_Window* window) { VkSurfaceKHR createWindowSurface(void* context, VkInstance instance) { VkSurfaceKHR surface = VK_NULL_HANDLE; +#ifdef __APPLE__ + RunOnMainThread([&] { + if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError()); + }); +#else if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError()); +#endif return surface; } @@ -299,7 +308,13 @@ VideoOutDriver::VideoOutDriver() { #ifdef __linux__ SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); #endif +#ifdef __APPLE__ + int initialized = 0; + RunOnMainThread([&] { initialized = SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); }); + if (initialized < 0) { +#else if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { +#endif throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); } try { @@ -696,7 +711,12 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } SDL_Event event; +#ifdef __APPLE__ + RunOnMainThread([] { SDL_PumpEvents(); }); + while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) > 0) { +#else while (SDL_PollEvent(&event)) { +#endif if (event.type == SDL_QUIT) { LibcRequestExit_nid_postfix(0); throw ProcessShutdown{}; diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index bf8e7ab043..5da6f7212e 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -46,6 +46,8 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/x86_64/CallTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestImages.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestTls.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestStart.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AllocatingStrings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/WideConversion.cpp @@ -171,6 +173,10 @@ if(WIN32) target_link_libraries(libc PRIVATE advapi32 ws2_32 iphlpapi) endif() +if(APPLE) + target_link_libraries(libc PRIVATE "-framework CoreFoundation") +endif() + if(BUILD_TESTING) if(UNIX) add_test_executable(guest_fopen_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestFopenErrors.cpp) diff --git a/core/libs/prx/libc/include/MainThread.hpp b/core/libs/prx/libc/include/MainThread.hpp new file mode 100644 index 0000000000..e68b3d52fd --- /dev/null +++ b/core/libs/prx/libc/include/MainThread.hpp @@ -0,0 +1,37 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_MAINTHREAD_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_MAINTHREAD_HPP + +#ifdef __APPLE__ + +#include +#include +#include +#include + +extern "C" bool LibcMainQueueServed_nid_postfix(); + +template +void RunOnMainThread(Work&& work) { + if (pthread_main_np() != 0 || !LibcMainQueueServed_nid_postfix()) { + work(); + return; + } + using Callable = std::remove_reference_t; + struct Context { + Callable* Call; + std::exception_ptr Failure; + } context{&work, nullptr}; + dispatch_sync_f(dispatch_get_main_queue(), &context, [](void* argument) { + auto* call = static_cast(argument); + try { + (*call->Call)(); + } catch (...) { + call->Failure = std::current_exception(); + } + }); + if (context.Failure) std::rethrow_exception(context.Failure); +} + +#endif + +#endif diff --git a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp index 5efa1f00d3..8a556bd0e0 100644 --- a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp +++ b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp @@ -10,7 +10,7 @@ #define NOMINMAX #endif #include -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include #endif @@ -77,7 +77,7 @@ const void* ApplicationProcessParameters_nid_no_patch() { } if (result == nullptr) throw std::runtime_error("application heap: process metadata is missing; relink the executable"); return result; -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) struct Search { const void* parameters = nullptr; bool invalid = false; diff --git a/core/libs/prx/libc/src/specifics/macos/GuestStart.cpp b/core/libs/prx/libc/src/specifics/macos/GuestStart.cpp new file mode 100644 index 0000000000..c5eaa85ae1 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/macos/GuestStart.cpp @@ -0,0 +1,75 @@ +#ifdef __APPLE__ + + +#include "prx/libc/include/general/AsmMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" [[noreturn]] void Aps5EnterGuest_nid_no_patch(void* entry, void* block); + +asm(".text\n" + ".p2align 4\n" APS5_ASM_FUNCTION("Aps5EnterGuest_nid_no_patch") " movq %rdi, %rax\n" + " movq %rsi, %rdi\n" + " andq $-16, %rsp\n" + " pushq $0\n" + " pushq $0\n" + " movq %rsp, %rbp\n" + " xorl %esi, %esi\n" + " call *%rax\n" + " movl %eax, %edi\n" + " call " APS5_ASM_SYMBOL("exit") "\n" + " ud2\n" + APS5_ASM_FUNCTION_END("Aps5EnterGuest_nid_no_patch")); + +namespace { + +constexpr std::size_t GuestMainStackSize = std::size_t{64} << 20; + +std::atomic mainQueueServed{false}; + +struct GuestStart { + void* Entry; + void* Block; +}; + +void* RunGuestMain(void* argument) { + const auto start = *static_cast(argument); + pthread_setname_np("main"); + Aps5EnterGuest_nid_no_patch(start.Entry, start.Block); +} + +[[noreturn]] void Fatal(const char* message, int error) { + std::fprintf(stderr, "guest start: %s (%d)\n", message, error); + std::abort(); +} + +} + +extern "C" bool LibcMainQueueServed_nid_postfix() { + return mainQueueServed.load(std::memory_order_acquire); +} + +extern "C" [[noreturn]] void Aps5StartGuest_nid_no_patch(void* entry, void* block) { + static GuestStart start; + start = {entry, block}; + pthread_attr_t attributes; + int result = pthread_attr_init(&attributes); + if (result != 0) Fatal("pthread_attr_init failed", result); + result = pthread_attr_setstacksize(&attributes, GuestMainStackSize); + if (result != 0) Fatal("pthread_attr_setstacksize failed", result); + result = pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED); + if (result != 0) Fatal("pthread_attr_setdetachstate failed", result); + mainQueueServed.store(true, std::memory_order_release); + pthread_t thread; + result = pthread_create(&thread, &attributes, RunGuestMain, &start); + if (result != 0) Fatal("pthread_create failed", result); + pthread_attr_destroy(&attributes); + for (;;) CFRunLoopRun(); +} + +#endif diff --git a/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp b/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp new file mode 100644 index 0000000000..a7d6547f37 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp @@ -0,0 +1,161 @@ +#ifdef __APPLE__ + + +#include "prx/libc/include/general/AsmMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct GuestTlsDescriptor { + std::uint64_t Active; + std::uint64_t InitImage; + std::uint64_t FileSize; + std::uint64_t BlockSize; + std::uint64_t Alignment; + std::uint64_t Key; +}; +static_assert(sizeof(GuestTlsDescriptor) == 0x30); + +constexpr std::size_t ThreadControlBlockSize = 0x30; +constexpr std::size_t AllocationSlot = 0x08; +constexpr std::size_t StackGuardSlot = 0x28; + +[[noreturn]] void Fatal(const char* message) { + std::fprintf(stderr, "guest TLS: %s\n", message); + std::abort(); +} + +std::mutex& RegistrationLock() { + static std::mutex lock; + return lock; +} + +void FreeBlock(void* threadPointer) { + if (threadPointer == nullptr) return; + void* block = nullptr; + std::memcpy(&block, static_cast(threadPointer) + AllocationSlot, sizeof(block)); + std::free(block); +} + +std::uint64_t StackGuardValue() { + static const std::uint64_t value = [] { + std::uint64_t guard = 0; + arc4random_buf(&guard, sizeof(guard)); + return guard & ~std::uint64_t{0xff}; + }(); + return value; +} + +} + +extern "C" std::uint8_t Aps5GuestTlsUseXsave = [] { + unsigned eax = 0, ebx = 0, ecx = 0, edx = 0; + if (!__get_cpuid(1, &eax, &ebx, &ecx, &edx)) return std::uint8_t{0}; + return static_cast((ecx & bit_OSXSAVE) != 0); +}(); + +extern "C" void Aps5GuestTlsRegister_nid_no_patch(GuestTlsDescriptor* descriptor) { + std::lock_guard lock(RegistrationLock()); + if (descriptor->Key != 0) return; + pthread_key_t key{}; + if (pthread_key_create(&key, FreeBlock) != 0) Fatal("pthread_key_create failed"); + if (key == 0) Fatal("pthread_key_create returned key 0"); + descriptor->Key = key; +} + +extern "C" void* Aps5GuestTlsAllocateBlock_nid_no_patch(GuestTlsDescriptor* descriptor) { + if (descriptor->Key == 0) Aps5GuestTlsRegister_nid_no_patch(descriptor); + const auto key = static_cast(descriptor->Key); + if (void* existing = pthread_getspecific(key)) return existing; + const std::size_t alignment = descriptor->Alignment > 16 ? descriptor->Alignment : 16; + const std::size_t tlsSize = (descriptor->BlockSize + alignment - 1) & ~(alignment - 1); + const std::size_t total = tlsSize + ThreadControlBlockSize; + void* block = nullptr; + if (posix_memalign(&block, alignment, total) != 0 || block == nullptr) Fatal("out of memory for a thread block"); + auto* bytes = static_cast(block); + std::memset(bytes, 0, total); + auto* threadPointer = bytes + tlsSize; + auto* image = threadPointer - descriptor->BlockSize; + if (descriptor->FileSize != 0 && descriptor->InitImage != 0) + std::memcpy(image, reinterpret_cast(descriptor->InitImage), descriptor->FileSize); + void* self = threadPointer; + std::memcpy(threadPointer, &self, sizeof(self)); + std::memcpy(threadPointer + AllocationSlot, &block, sizeof(block)); + const auto guard = StackGuardValue(); + std::memcpy(threadPointer + StackGuardSlot, &guard, sizeof(guard)); + if (pthread_setspecific(key, threadPointer) != 0) Fatal("pthread_setspecific failed"); + return threadPointer; +} + +struct GuestTlsIndex { + std::uint64_t Module; + std::uint64_t Offset; +}; + +extern "C" void* Aps5GuestTlsGetAddr_nid_no_patch(const GuestTlsIndex* index) { + auto* descriptor = reinterpret_cast(index->Module); + if (descriptor == nullptr) Fatal("__tls_get_addr with no module"); + auto* threadPointer = static_cast( + descriptor->Key != 0 ? pthread_getspecific(static_cast(descriptor->Key)) : nullptr); + if (threadPointer == nullptr) threadPointer = static_cast(Aps5GuestTlsAllocateBlock_nid_no_patch(descriptor)); + return threadPointer - descriptor->BlockSize + index->Offset; +} + +asm( + ".text\n" + ".p2align 4\n" + APS5_ASM_FUNCTION("Aps5GuestTlsAllocate_nid_no_patch") + " pushq %rbp\n" + " movq %rsp, %rbp\n" + " pushq %rcx\n" + " pushq %rdx\n" + " pushq %rsi\n" + " pushq %rdi\n" + " pushq %r8\n" + " pushq %r9\n" + " pushq %r10\n" + " pushq %r11\n" + " movq %rax, %rdi\n" + " andq $-64, %rsp\n" + " subq $4096, %rsp\n" + " cmpb $0, " APS5_ASM_SYMBOL("Aps5GuestTlsUseXsave") "(%rip)\n" + " je 1f\n" + " movq %rsp, %rcx\n" + " movl $512, %edx\n" + "2: movq $0, (%rcx,%rdx)\n" + " addq $8, %rdx\n" + " cmpq $576, %rdx\n" + " jb 2b\n" + " movl $-1, %eax\n" + " movl $-1, %edx\n" + " xsave64 (%rsp)\n" + " call " APS5_ASM_SYMBOL("Aps5GuestTlsAllocateBlock_nid_no_patch") "\n" + " movq %rax, %rsi\n" + " movl $-1, %eax\n" + " movl $-1, %edx\n" + " xrstor64 (%rsp)\n" + " movq %rsi, %rax\n" + " jmp 3f\n" + "1: fxsave64 (%rsp)\n" + " call " APS5_ASM_SYMBOL("Aps5GuestTlsAllocateBlock_nid_no_patch") "\n" + " fxrstor64 (%rsp)\n" + "3: leaq -64(%rbp), %rsp\n" + " popq %r11\n" + " popq %r10\n" + " popq %r9\n" + " popq %r8\n" + " popq %rdi\n" + " popq %rsi\n" + " popq %rdx\n" + " popq %rcx\n" + " popq %rbp\n" + " ret\n" + APS5_ASM_FUNCTION_END("Aps5GuestTlsAllocate_nid_no_patch")); + +#endif diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index b864c5640e..624d64518b 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -816,7 +816,8 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { std::vector file; constexpr std::size_t HeaderSize = 32; - const auto stubOffset = AlignUp(HeaderSize + commands.size(), 16); + constexpr std::size_t LoadCommandReserve = 0x400; + const auto stubOffset = AlignUp(HeaderSize + commands.size() + LoadCommandReserve, 16); if (stubOffset + (input.Executable ? sizeof(EntryStub) : 0) > LoadRva) throw Domain::RelinkerException("Mach-O load commands do not fit before the guest image"); file.resize(LoadRva, 0); From 988eabf3dc3448b9cc1d51c267ec72f1ebe8d0d5 Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Mon, 26 Oct 2026 05:48:54 +0530 Subject: [PATCH 099/702] fix(macos): publish the guest TLS key atomically Aps5GuestTlsAllocateBlock and Aps5GuestTlsGetAddr read the descriptor's Key outside the registration mutex while another thread's first access could be writing it, a data race that could also expose a key before pthread_key_create had finished. Every access now goes through RegisteredKey: an acquire load through std::atomic_ref on the unchanged descriptor field, and on a zero key a double-checked creation under the mutex that publishes with a release store. macos_guest_tls_first_access races the first access from 16 threads, 100 rounds through GetAddr and 100 through AllocateBlock, and checks for one non-zero key, a distinct block per thread and a stable second access. --- core/libs/prx/libc/CMakeLists.txt | 8 ++ .../prx/libc/src/specifics/macos/GuestTls.cpp | 31 +++++--- core/libs/tests/MacOsGuestTlsFirstAccess.cpp | 76 +++++++++++++++++++ 3 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 core/libs/tests/MacOsGuestTlsFirstAccess.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 5da6f7212e..dd42141b37 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -175,6 +175,14 @@ endif() if(APPLE) target_link_libraries(libc PRIVATE "-framework CoreFoundation") + if(BUILD_TESTING) + add_test_executable(macos_guest_tls_first_access_tests + ${LIBS_INCLUDE_DIR}/tests/MacOsGuestTlsFirstAccess.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestTls.cpp) + target_include_directories(macos_guest_tls_first_access_tests PRIVATE ${LIBS_INCLUDE_DIR}) + add_test(NAME macos_guest_tls_first_access COMMAND macos_guest_tls_first_access_tests) + set_tests_properties(macos_guest_tls_first_access PROPERTIES TIMEOUT 60) + endif() endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp b/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp index a7d6547f37..e29f4171f1 100644 --- a/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp +++ b/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/AsmMacros.hpp" +#include #include #include #include @@ -43,6 +44,23 @@ void FreeBlock(void* threadPointer) { std::free(block); } +std::atomic_ref KeyOf(GuestTlsDescriptor* descriptor) { + static_assert(std::atomic_ref::is_always_lock_free); + return std::atomic_ref(descriptor->Key); +} + +pthread_key_t RegisteredKey(GuestTlsDescriptor* descriptor) { + auto key = KeyOf(descriptor); + if (const auto value = key.load(std::memory_order_acquire); value != 0) return static_cast(value); + std::lock_guard lock(RegistrationLock()); + if (const auto value = key.load(std::memory_order_relaxed); value != 0) return static_cast(value); + pthread_key_t created{}; + if (pthread_key_create(&created, FreeBlock) != 0) Fatal("pthread_key_create failed"); + if (created == 0) Fatal("pthread_key_create returned key 0"); + key.store(created, std::memory_order_release); + return created; +} + std::uint64_t StackGuardValue() { static const std::uint64_t value = [] { std::uint64_t guard = 0; @@ -61,17 +79,11 @@ extern "C" std::uint8_t Aps5GuestTlsUseXsave = [] { }(); extern "C" void Aps5GuestTlsRegister_nid_no_patch(GuestTlsDescriptor* descriptor) { - std::lock_guard lock(RegistrationLock()); - if (descriptor->Key != 0) return; - pthread_key_t key{}; - if (pthread_key_create(&key, FreeBlock) != 0) Fatal("pthread_key_create failed"); - if (key == 0) Fatal("pthread_key_create returned key 0"); - descriptor->Key = key; + RegisteredKey(descriptor); } extern "C" void* Aps5GuestTlsAllocateBlock_nid_no_patch(GuestTlsDescriptor* descriptor) { - if (descriptor->Key == 0) Aps5GuestTlsRegister_nid_no_patch(descriptor); - const auto key = static_cast(descriptor->Key); + const auto key = RegisteredKey(descriptor); if (void* existing = pthread_getspecific(key)) return existing; const std::size_t alignment = descriptor->Alignment > 16 ? descriptor->Alignment : 16; const std::size_t tlsSize = (descriptor->BlockSize + alignment - 1) & ~(alignment - 1); @@ -101,8 +113,7 @@ struct GuestTlsIndex { extern "C" void* Aps5GuestTlsGetAddr_nid_no_patch(const GuestTlsIndex* index) { auto* descriptor = reinterpret_cast(index->Module); if (descriptor == nullptr) Fatal("__tls_get_addr with no module"); - auto* threadPointer = static_cast( - descriptor->Key != 0 ? pthread_getspecific(static_cast(descriptor->Key)) : nullptr); + auto* threadPointer = static_cast(pthread_getspecific(RegisteredKey(descriptor))); if (threadPointer == nullptr) threadPointer = static_cast(Aps5GuestTlsAllocateBlock_nid_no_patch(descriptor)); return threadPointer - descriptor->BlockSize + index->Offset; } diff --git a/core/libs/tests/MacOsGuestTlsFirstAccess.cpp b/core/libs/tests/MacOsGuestTlsFirstAccess.cpp new file mode 100644 index 0000000000..6d2b6c0f5f --- /dev/null +++ b/core/libs/tests/MacOsGuestTlsFirstAccess.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include +#include +#include + +struct GuestTlsDescriptor { + std::uint64_t Active; + std::uint64_t InitImage; + std::uint64_t FileSize; + std::uint64_t BlockSize; + std::uint64_t Alignment; + std::uint64_t Key; +}; +static_assert(sizeof(GuestTlsDescriptor) == 0x30); + +struct GuestTlsIndex { + std::uint64_t Descriptor; + std::uint64_t Offset; +}; + +extern "C" void* Aps5GuestTlsGetAddr_nid_no_patch(const GuestTlsIndex* index); +extern "C" void* Aps5GuestTlsAllocateBlock_nid_no_patch(GuestTlsDescriptor* descriptor); + +namespace { + +void Require(bool condition, const char* message, int round) { + if (condition) return; + std::fprintf(stderr, "round %d: %s\n", round, message); + std::exit(1); +} + +void RaceFirstAccess(int round, bool throughIndex) { + constexpr int Threads = 16; + alignas(64) static GuestTlsDescriptor descriptor{}; + descriptor = GuestTlsDescriptor{1, 0, 0, 64, 16, 0}; + std::atomic ready{0}; + std::atomic recorded{0}; + std::atomic start{false}; + std::vector blocks(Threads, nullptr); + std::vector again(Threads, nullptr); + std::vector keys(Threads, 0); + std::vector pool; + for (int thread = 0; thread < Threads; ++thread) + pool.emplace_back([&, thread] { + ready.fetch_add(1); + while (!start.load(std::memory_order_acquire)) {} + GuestTlsIndex index{reinterpret_cast(&descriptor), 8}; + blocks[thread] = throughIndex ? Aps5GuestTlsGetAddr_nid_no_patch(&index) + : Aps5GuestTlsAllocateBlock_nid_no_patch(&descriptor); + keys[thread] = std::atomic_ref(descriptor.Key).load(std::memory_order_acquire); + again[thread] = throughIndex ? Aps5GuestTlsGetAddr_nid_no_patch(&index) + : Aps5GuestTlsAllocateBlock_nid_no_patch(&descriptor); + recorded.fetch_add(1); + while (recorded.load() != Threads) {} + }); + while (ready.load() != Threads) {} + start.store(true, std::memory_order_release); + for (auto& thread : pool) thread.join(); + const std::set distinctKeys(keys.begin(), keys.end()); + const std::set distinctBlocks(blocks.begin(), blocks.end()); + Require(distinctKeys.size() == 1 && *distinctKeys.begin() != 0, "racing first accesses registered more than one key", round); + Require(distinctBlocks.size() == Threads && distinctBlocks.count(nullptr) == 0, "threads did not get distinct blocks", round); + for (int thread = 0; thread < Threads; ++thread) + Require(again[thread] == blocks[thread], "a thread's second access returned a different block", round); +} + +} + +int main() { + for (int round = 0; round < 100; ++round) RaceFirstAccess(round, true); + for (int round = 0; round < 100; ++round) RaceFirstAccess(round, false); + return 0; +} From 344acf3e661e8b1a240f2cd0436df76d3c31c9b8 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:28:44 -0400 Subject: [PATCH 100/702] docs(libSceRudp): link the detailed 13.42 report for sceRudpPollCancel Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceRudp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index 48736eb03a..723183e673 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -12,5 +12,5 @@ - [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. - [sceRudpGetOption, sceRudpSetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - full PS5 option contracts are unverified; defaults and validation follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). Options 12 and 13 have unknown meanings; last-error option 14 is not maintained. Their setters store all 32 bits per context and return 0 for valid four-byte buffers, preserving acceptance on `main`; their getters throw. Static inspection of PPSA06888 v01.001.008's bundled Unity transport shows its context-set constructor configuring IDs 2, 3, 5 and 6; runtime usage and other paths remain unverified. The plugin wrappers pass four-byte values. No context can connect, so stored settings cannot affect traffic. Closes when: PS5 option contracts are verified and the remaining behavior is implemented. - [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5; imported by PPSA31327 and not called before the main menu. Signatures and the 8-byte event layout are recovered from the supplied module. The operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp), and the error codes match [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented, so `int sceRudpPollDestroy(int pollId)` and its checks are inferred. No context can connect, so no event is ever reported: `sceRudpPollWait` returns 0 when its timeout elapses, also for an empty poll, where Vita3K returns at once -- [sceRudpPollCancel](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5. `int sceRudpPollCancel(int pollId)` and its behaviour follow @BunnyFl's run of firmware 13.42's `libSceRudp.sprx` in [#10954](https://github.com/boykopovar/AnyPS5/pull/10954), which executed the library with its kernel and libc imports stubbed; it is not a console measurement. Before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED`, for a negative id `RUDP_ERROR_INVALID_ARGUMENT` and for an unknown id `RUDP_ERROR_INVALID_POLL_ID`. With no waiter, one cancellation is kept for that poll however often it is cancelled, and the next `sceRudpPollWait` on it, also with a zero timeout, returns `0x80770014` at once. With waiters, all of them return `0x80770014` and the call returns 0 once none is left; nothing stays pending. How a cancellation races a timeout is unverified. +- [sceRudpPollCancel](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5. `int sceRudpPollCancel(int pollId)` and its behaviour follow @BunnyFl's run of firmware 13.42's `libSceRudp.sprx` in [#10954](https://github.com/boykopovar/AnyPS5/pull/10954#issuecomment-6399054346), which executed the library with its kernel and libc imports stubbed; it is not a console measurement. Before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED`, for a negative id `RUDP_ERROR_INVALID_ARGUMENT` and for an unknown id `RUDP_ERROR_INVALID_POLL_ID`. With no waiter, one cancellation is kept for that poll however often it is cancelled, and the next `sceRudpPollWait` on it, also with a zero timeout, returns `0x80770014` at once. With waiters, all of them return `0x80770014` and the call returns 0 once none is left; nothing stays pending. How a cancellation races a timeout is unverified. - [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From ee2bfe5dbb9a1053d096f334e9b04381f8e75fba Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Mon, 26 Oct 2026 06:04:00 +0530 Subject: [PATCH 101/702] fix(macos): query the window from the main thread in VideoOut The presentation thread still queried the SDL window directly on macOS: SDL_Vulkan_GetInstanceExtensions in windowExtensions, SDL_GetWindowFlags and SDL_Vulkan_GetDrawableSize in windowDrawableSize, SDL_GetWindowSize after each flip, and SDL_GetWindowDisplayIndex with SDL_GetCurrentDisplayMode in updateDisplayRefreshRate. Each now runs through RunOnMainThread on macOS, the same way window creation, the Vulkan surface and event pumping already do. --- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index b97cdafcf4..7a5357c8be 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -67,12 +67,24 @@ std::optional configuredResolution() { } std::vector windowExtensions(SDL_Window* window) { +#ifdef __APPLE__ + std::vector extensions; + RunOnMainThread([&] { + unsigned extensionCount = 0; + if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); + extensions.resize(extensionCount); + if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); + extensions.resize(extensionCount); + }); + return extensions; +#else unsigned extensionCount = 0; if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); std::vector extensions(extensionCount); if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); extensions.resize(extensionCount); return extensions; +#endif } VkSurfaceKHR createWindowSurface(void* context, VkInstance instance) { @@ -88,6 +100,20 @@ VkSurfaceKHR createWindowSurface(void* context, VkInstance instance) { } void windowDrawableSize(void* context, std::uint32_t* width, std::uint32_t* height) { +#ifdef __APPLE__ + bool minimized = false; + int drawableWidth = 0; + int drawableHeight = 0; + RunOnMainThread([&] { + minimized = (SDL_GetWindowFlags(static_cast(context)) & SDL_WINDOW_MINIMIZED) != 0; + if (!minimized) SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); + }); + if (minimized) { + *width = 0; + *height = 0; + return; + } +#else if ((SDL_GetWindowFlags(static_cast(context)) & SDL_WINDOW_MINIMIZED) != 0) { *width = 0; *height = 0; @@ -96,6 +122,7 @@ void windowDrawableSize(void* context, std::uint32_t* width, std::uint32_t* heig int drawableWidth = 0; int drawableHeight = 0; SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); +#endif *width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; *height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; } @@ -532,10 +559,19 @@ std::uint32_t VideoOutDriver::GetDisplayResolution() { } void VideoOutDriver::updateDisplayRefreshRate() { +#ifdef __APPLE__ + SDL_DisplayMode mode{}; + RunOnMainThread([&] { + const int display = SDL_GetWindowDisplayIndex(window.Handle()); + if (display < 0) throw std::runtime_error(std::string("SDL_GetWindowDisplayIndex failed: ") + SDL_GetError()); + if (SDL_GetCurrentDisplayMode(display, &mode) != 0) throw std::runtime_error(std::string("SDL_GetCurrentDisplayMode failed: ") + SDL_GetError()); + }); +#else const int display = SDL_GetWindowDisplayIndex(window.Handle()); if (display < 0) throw std::runtime_error(std::string("SDL_GetWindowDisplayIndex failed: ") + SDL_GetError()); SDL_DisplayMode mode{}; if (SDL_GetCurrentDisplayMode(display, &mode) != 0) throw std::runtime_error(std::string("SDL_GetCurrentDisplayMode failed: ") + SDL_GetError()); +#endif displayRefreshRate.store(mode.refresh_rate); displayResolution.store(resolutionIndex(mode.w, mode.h)); } @@ -739,7 +775,11 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star processFlip(*current); int windowWidth = 0; int windowHeight = 0; +#ifdef __APPLE__ + if (window.Handle() != nullptr) RunOnMainThread([&] { SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); }); +#else if (window.Handle() != nullptr) SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); +#endif if (windowWidth > 0 && windowHeight > 0) { const auto presented = AgcDriver::ComputeContainSize_nid_postfix(current->width, current->height, static_cast(windowWidth), static_cast(windowHeight), true); mouseInput.SetPresentation(current->width, current->height, presented.width, presented.height); From d67dc5ba43d616b34a4929df230adbc732b9fdbb Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Mon, 26 Oct 2026 06:06:22 +0530 Subject: [PATCH 102/702] fix(macos): set relative mouse mode from the main thread MouseInput::UpdateCapture and PadInput::setMouseMode switch SDL's relative mouse mode, which on macOS warps and hides the cursor through the window, from the presentation thread. On macOS both now read and set it through RunOnMainThread. --- core/libs/prx/libSceVideoOut/src/MouseInput.cpp | 12 ++++++++++++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index f1e79af366..01e9e32243 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -5,6 +5,9 @@ #include "SDL_video.h" #include #include +#ifdef __APPLE__ +#include "prx/libc/include/MainThread.hpp" +#endif void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { MouseInputEvent input{}; @@ -87,8 +90,17 @@ void MouseInput::UpdateCapture(bool allowed) { return; } const bool wanted = CaptureWanted(allowed); +#ifdef __APPLE__ + bool relative = false; + RunOnMainThread([&] { relative = SDL_GetRelativeMouseMode() == SDL_TRUE; }); + if (wanted == captured && relative == wanted) return; + int result = 0; + RunOnMainThread([&] { result = SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE); }); + if (result != 0) { +#else if (wanted == captured && (SDL_GetRelativeMouseMode() == SDL_TRUE) == wanted) return; if (SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE) != 0) { +#endif static bool warned = false; if (!warned) { warned = true; diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 5e9ac3fcf7..7a61eb88ab 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -12,6 +12,9 @@ #include "prx/libSceMouse/include/MouseState.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" +#ifdef __APPLE__ +#include "prx/libc/include/MainThread.hpp" +#endif #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/Pad.hpp" @@ -168,7 +171,13 @@ void PadInput::applyOutput() { void PadInput::setMouseMode(bool enabled) { SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0"); SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0"); +#ifdef __APPLE__ + int result = 0; + RunOnMainThread([&] { result = SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE); }); + if (result != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError()); +#else if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError()); +#endif keyboardMouse.SetMouseMode(enabled); } From 38a945da2b1c6a576a0399c482d732de318ebde3 Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Mon, 26 Oct 2026 06:53:06 +0530 Subject: [PATCH 103/702] fix(macos): keep VideoOut window queries and SDL errors on the main thread On macOS: - ToggleFullscreen reads the window flags and sets fullscreen in one main-thread call, instead of reading the flags on the presentation thread. - DisplayWindow records SDL_GetWindowID while creating the window on the main thread and exposes it as Id(). The mouse, keyboard, IME and pad event routing compares against it instead of calling SDL_GetWindowID from the presentation thread. - SDL_InitSubSystem, PadInput::setMouseMode and MouseInput::UpdateCapture copy SDL_GetError inside the main-thread call that failed and report that copy, since SDL error strings are per thread. --- .../prx/libSceVideoOut/include/DisplayWindow.hpp | 6 ++++++ .../prx/libSceVideoOut/src/DisplayWindow.cpp | 11 +++++++++-- core/libs/prx/libSceVideoOut/src/MouseInput.cpp | 16 ++++++++++++++-- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 14 +++++++++++--- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 16 ++++++++++++---- 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp index 63ba3f0281..1c86d3c7b4 100644 --- a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp +++ b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp @@ -22,6 +22,9 @@ public: void UpdateTitle(); std::uint64_t FrameNumber() const { return frameNum; } void ToggleFullscreen(); +#ifdef __APPLE__ + Uint32 Id() const { return windowId; } +#endif private: void create(std::uint32_t sourceWidth, std::uint32_t sourceHeight); @@ -36,6 +39,9 @@ private: std::uint32_t aspectWidth = 0; std::uint32_t aspectHeight = 0; std::uint64_t frameNum = 0; +#ifdef __APPLE__ + Uint32 windowId = 0; +#endif }; #endif diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index 30fdf21f22..b39076dd78 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -71,6 +71,9 @@ void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight window = SDL_CreateWindow(title.value, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(initialSize.width), static_cast(initialSize.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); AgcDriverUnlockVulkanLoader_nid_postfix(); require(window != nullptr, SDL_GetError()); +#ifdef __APPLE__ + windowId = SDL_GetWindowID(window); +#endif SDL_SetWindowMinimumSize(window, static_cast(DisplayWindowMinimumWidth), static_cast(DisplayWindowMinimumHeight)); applyAppIcon(window); installSubclass(); @@ -89,6 +92,7 @@ void DisplayWindow::Destroy() noexcept { removeSubclass(); #ifdef __APPLE__ RunOnMainThread([&] { SDL_DestroyWindow(window); }); + windowId = 0; #else SDL_DestroyWindow(window); #endif @@ -101,10 +105,13 @@ SDL_Window* DisplayWindow::Handle() const { void DisplayWindow::ToggleFullscreen() { require(window != nullptr, "window must exist before toggling fullscreen"); - const auto flags = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0 ? 0u : static_cast(SDL_WINDOW_FULLSCREEN_DESKTOP); #ifdef __APPLE__ - RunOnMainThread([&] { require(SDL_SetWindowFullscreen(window, flags) == 0, SDL_GetError()); }); + RunOnMainThread([&] { + const auto flags = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0 ? 0u : static_cast(SDL_WINDOW_FULLSCREEN_DESKTOP); + require(SDL_SetWindowFullscreen(window, flags) == 0, SDL_GetError()); + }); #else + const auto flags = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0 ? 0u : static_cast(SDL_WINDOW_FULLSCREEN_DESKTOP); require(SDL_SetWindowFullscreen(window, flags) == 0, SDL_GetError()); #endif } diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index 01e9e32243..aa084f088b 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -6,6 +6,7 @@ #include #include #ifdef __APPLE__ +#include #include "prx/libc/include/MainThread.hpp" #endif @@ -95,12 +96,22 @@ void MouseInput::UpdateCapture(bool allowed) { RunOnMainThread([&] { relative = SDL_GetRelativeMouseMode() == SDL_TRUE; }); if (wanted == captured && relative == wanted) return; int result = 0; - RunOnMainThread([&] { result = SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE); }); + std::string error; + RunOnMainThread([&] { + result = SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE); + if (result != 0) error = SDL_GetError(); + }); if (result != 0) { + static bool warned = false; + if (!warned) { + warned = true; + std::fprintf(stderr, "[mouse] relative mouse mode unavailable: %s\n", error.c_str()); + } + return; + } #else if (wanted == captured && (SDL_GetRelativeMouseMode() == SDL_TRUE) == wanted) return; if (SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE) != 0) { -#endif static bool warned = false; if (!warned) { warned = true; @@ -108,6 +119,7 @@ void MouseInput::UpdateCapture(bool allowed) { } return; } +#endif int ignoredX = 0; int ignoredY = 0; SDL_GetRelativeMouseState(&ignoredX, &ignoredY); diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 7a61eb88ab..516a806728 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -172,9 +172,11 @@ void PadInput::setMouseMode(bool enabled) { SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0"); SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0"); #ifdef __APPLE__ - int result = 0; - RunOnMainThread([&] { result = SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE); }); - if (result != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError()); + std::string error; + RunOnMainThread([&] { + if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) error = SDL_GetError(); + }); + if (!error.empty()) throw std::runtime_error("Pad: relative mouse mode failed: " + error); #else if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError()); #endif @@ -300,9 +302,15 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (event.type == SDL_KEYDOWN && ImeCapturesKeyboard_nid_postfix()) return; keyboardMouse.SetTitleReadsMouse(MouseIsOpen_nid_no_patch()); const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()); +#ifdef __APPLE__ + if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == window.Id()) window.ToggleFullscreen(); + if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == window.Id()) AgcDriverTogglePerformanceOverlay_nid_postfix(); + if (requests.screenshot && window.Handle() != nullptr && event.key.windowID == window.Id()) AgcDriverRequestScreenshot_nid_postfix(); +#else if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix(); if (requests.screenshot && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverRequestScreenshot_nid_postfix(); +#endif if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); if (event.type != SDL_MOUSEMOTION) publish(window.FrameNumber(), 0); } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 7a5357c8be..1992f2513d 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -336,14 +336,16 @@ VideoOutDriver::VideoOutDriver() { SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); #endif #ifdef __APPLE__ - int initialized = 0; - RunOnMainThread([&] { initialized = SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); }); - if (initialized < 0) { + std::string initError; + RunOnMainThread([&] { + if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) initError = SDL_GetError(); + }); + if (!initError.empty()) throw std::runtime_error("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: " + initError); #else if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { -#endif throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); } +#endif try { AgcDriverWaitIdle_nid_postfix(); std::promise started; @@ -759,9 +761,15 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } padInput.HandleEvent(event, window); if (window.Handle() != nullptr) { +#ifdef __APPLE__ + mouseInput.HandleEvent(event, window.Id()); + keyboardInput.HandleEvent(event, window.Id()); + imeTextInput.HandleEvent(event, window.Id()); +#else mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); keyboardInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); imeTextInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); +#endif } } padInput.Update(window); From 8a9a741b684469cce9997266ed44d7e2d0e53aea Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Mon, 26 Oct 2026 07:19:28 +0530 Subject: [PATCH 104/702] fix(agc): clear a reused pooled buffer's unused tail on macOS MoltenVK has no robustBufferAccess2, so a robust read past the end of a pooled buffer is clamped into its slot instead of returning zero. A 100-byte buffer placed in a 256-byte slot then read the bytes an earlier user left there (0xabababab), and agc_driver_pooled_buffer_range failed. On macOS, when a host-coherent mapped slot is handed to a new buffer, the bytes past the requested size are cleared to zero. Linux and Windows compile the same code as before. --- .../libSceAgcDriver/Graphics/src/Resources.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index ea37fed7ef..8dbb767364 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -6,11 +6,22 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include +#ifdef __APPLE__ +#include +#include +#endif namespace AgcDriver::Graphics { namespace { +#ifdef __APPLE__ +void ClearPooledTail(void* mapping, std::size_t size, VkDeviceSize mappedBytes, VkMemoryPropertyFlags properties) { + if (mapping == nullptr || (properties & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0 || mappedBytes <= size) return; + std::memset(static_cast(mapping) + size, 0, static_cast(mappedBytes - size)); +} + +#endif VkDeviceSize ClassAllocationBytes(const VkMemoryRequirements& requirements, std::size_t capacity) { const auto alignment = std::max(requirements.alignment, 1); return std::max(requirements.size, (static_cast(capacity) + alignment - 1) / alignment * alignment); @@ -52,6 +63,9 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag capacity = allocation->bytes; offset = allocation->offset; slab = allocation->slab; +#ifdef __APPLE__ + ClearPooledTail(mapping, size, std::min(capacity, allocationBytes), properties); +#endif if (allocation->bufferBytes != size) { context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); buffer = VK_NULL_HANDLE; @@ -93,6 +107,9 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, offset), "vkBindBufferMemory slab"); initializeAddress(usage); mapping = slot.mapping; +#ifdef __APPLE__ + ClearPooledTail(mapping, size, capacity, properties); +#endif ready = true; return; } From 035d9c0821563ca12b5ca3f84697f30e3e75a463 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 23 Oct 2026 12:16:01 -0400 Subject: [PATCH 105/702] fix(libc): compute cbrt with FreeBSD msun's algorithm, as titles' libc.prx does cbrt returned the host's cbrt, whose last bit differs from the titles' copy on Windows on 99,933,841 of 320 million random arguments, about 31%. The titles' copy is FreeBSD msun's s_cbrt.c, and cbrt now computes it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 37 ++++++++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 23 ++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index d88fc6d00b..27219ce090 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -420,7 +420,42 @@ float APS5_VABI nearbyintf_nid_postfix(float x) { return std::nearbyintf(x); } long double APS5_VABI nearbyintl_nid_postfix(long double x) { return std::nearbyint(x); } double APS5_VABI nearbyint_nid_postfix(double x) { return std::nearbyint(x); } -double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } +double APS5_VABI cbrt_nid_postfix(double x) { + const std::uint32_t B1 = 715094163; + const std::uint32_t B2 = 696219795; + const double P0 = 1.87595182427177009643; + const double P1 = -1.88497979543377169875; + const double P2 = 1.621429720105354466140; + const double P3 = -0.758397934778766047437; + const double P4 = 0.145996192886612446982; + const std::uint64_t bits = std::bit_cast(x); + std::uint32_t hx = static_cast(bits >> 32); + const std::uint32_t low = static_cast(bits); + const std::uint32_t sign = hx & 0x80000000u; + hx ^= sign; + if (hx >= 0x7ff00000) { + return x + x; + } + double t; + if (hx < 0x00100000) { + if ((hx | low) == 0) { + return x; + } + t = std::bit_cast(std::uint64_t{0x4350000000000000}) * x; + const std::uint32_t high = static_cast(std::bit_cast(t) >> 32); + t = std::bit_cast(static_cast(sign | ((high & 0x7fffffff) / 3 + B2)) << 32); + } else { + t = std::bit_cast(static_cast(sign | (hx / 3 + B1)) << 32); + } + double r = (t * t) * (t / x); + t = t * ((P0 + r * (P1 + r * P2)) + ((r * r) * r) * (P3 + r * P4)); + t = std::bit_cast((std::bit_cast(t) + 0x80000000u) & 0xffffffffc0000000ull); + const double s = t * t; + r = x / s; + const double w = t + t; + r = (r - t) / (w + r); + return t + t * r; +} double APS5_VABI asin_nid_postfix(double x) { if (std::isgreater(x, 1.0) || std::isless(x, -1.0)) { std::feraiseexcept(FE_INVALID); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 8297d06458..37203e50a3 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -1553,6 +1553,28 @@ static void CheckDinkumwareFloatCharacteristics() { for (std::size_t index = 24; index < 32; ++index) Require(reinterpret_cast(table)[index] == 0); } +static void CheckCbrtConsoleBits() { + constexpr std::uint64_t cases[][2] = { + {0x0000000000000000ull, 0x0000000000000000ull}, {0x8000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000001ull, 0x2990000000000000ull}, {0x8000000000000001ull, 0xa990000000000000ull}, + {0x000fffffffffffffull, 0x2aa428a2f98d728aull}, {0x0010000000000000ull, 0x2aa428a2f98d728bull}, + {0x3ff0000000000000ull, 0x3ff0000000000000ull}, {0xbff0000000000000ull, 0xbff0000000000000ull}, + {0x4020000000000000ull, 0x4000000000000000ull}, {0xc03b000000000000ull, 0xc008000000000000ull}, + {0x408f400000000000ull, 0x4024000000000000ull}, {0x4000000000000000ull, 0x3ff428a2f98d728bull}, + {0x3fe0000000000000ull, 0x3fe965fea53d6e3dull}, {0x7fefffffffffffffull, 0x554428a2f98d728bull}, + {0xffefffffffffffffull, 0xd54428a2f98d728bull}, {0x7ff0000000000000ull, 0x7ff0000000000000ull}, + {0xfff0000000000000ull, 0xfff0000000000000ull}, {0x7ff8000000000001ull, 0x7ff8000000000001ull}, + {0xfff8000000000002ull, 0xfff8000000000002ull}, {0x3ffa0ccfe147c94dull, 0x3ff2d29f09b6d6a3ull}, + {0xbf840fe48e07e54dull, 0xbfcb634d0e8f50ffull}, {0x3fa7d31e3ec31ee2ull, 0x3fd70508d1167185ull}, + {0xc050f6b81592ae3full, 0xc01050a4409443faull}, {0xc0506b7f30c60f3cull, 0xc0102385f7679661ull}, + {0xc039809ec6db32bcull, 0xc0078c3d13f2e055ull}, + }; + for (const auto& [input, expected] : cases) { + errno = 1234; + Require(std::bit_cast(cbrt_nid_postfix(std::bit_cast(input))) == expected && errno == 1234); + } +} + int main() { CheckFloatClassification(); CheckDinkumwareConstants(); @@ -1719,6 +1741,7 @@ int main() { CheckFegetenv(); CheckLongDouble(); CheckNearbyintl(); + CheckCbrtConsoleBits(); CheckExpm1ConsoleBits(); CheckLog1pConsoleBits(); CheckNextafterConsoleBits(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c648774059..17c5e5d9e5 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -185,3 +185,4 @@ - [fprintf_s, vfprintf_s, vprintf_s, fwprintf_s, wprintf_s, vfwprintf_s and vwprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follow C11 K.3.5.3.1, K.3.5.3.10 and K.3.9.1.1 with the constraints of `snprintf_s` and `swprintf_s`: a null stream, format or argument list, any `%n`, and a null `%s`, `%ls` or `%S` argument return -1 and write nothing, because the output is formatted completely before it is written. A violation only returns: the constraint handler is not invoked, `errno` is untouched, and the value is the one the narrow `snprintf_s` returns on this branch, not a result measured for a stream function on a PS5. A closed stream sets `EBADF` (9) as `fprintf` does. `vprintf_s` and `vwprintf_s` write to the guest standard output stream, and the wide forms count characters while the narrow ones count bytes, as their unchecked siblings do - [scanf_s, fscanf_s, vscanf_s and vfscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) run the scanner of `sscanf_s` over the stream, taking the size of `%s`, `%c` and `%[` as `unsigned int` after the pointer, so everything listed for `sscanf_s` applies, including the count of items converted before an undersized destination, whose first element is set to zero. Each directive is read from the stream with the host `fscanf`, with one character of look-ahead pushed back, so the stream stops after the last character a conversion used. A null stream, null format or null destination returns `EOF`, a closed stream also sets `EBADF`, and the constraint handler is not invoked. The 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) throw on Linux, as in `fscanf`. None of the stream forms were measured on a PS5, and the size type follows `sscanf_s` rather than `rsize_t` - [freopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows C11 K.3.5.2.2 through `freopen`: a null result, mode or stream returns `EINVAL` (22) with `*result` set to null when it exists, and a null name is passed to `freopen`, which reports mode-only reopening as unsupported (`ENOTSUP`, 45) with `*result` null, and the constraint handler is not invoked, as in `fopen_s`. A successful reopen stores the stream in `*result` and returns 0, and a failed one stores null and returns the errno `freopen` left, closing the stream as `freopen` does. The console's return values are unverified +- [cbrt](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_cbrt.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_cbrt.c), which sets no `errno`, as the titles' `libc.prx` does not. It gives the same bits as the `cbrt` bundled with PPSA07923 and PPSA05916 on 320 million random and 10,305 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 32 of the random ones. From 950d9401f1d0d891bad24858971b75fe07e5e9e6 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:53:38 +0100 Subject: [PATCH 106/702] fix(libc): read the stream status flags atomically when keeping a directory read error PublishStatus updates FileStream's guest status word with atomic operations from the thread that just read, while KeepDirectoryReadError read it with a plain load, a data race that ThreadSanitizer reports in guest_gets when two threads read the same stream. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/FileStream.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index a97f626f49..5747dd3fa7 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -282,7 +282,7 @@ public: } void KeepDirectoryReadError(bool failed) { - if (failed && directory && (_guest.flags & 0x40) != 0) errno = 21; + if (failed && directory && (__atomic_load_n(&_guest.flags, __ATOMIC_RELAXED) & 0x40) != 0) errno = 21; } void SetError() { From d77777a94bcbdd00c4de1137267593ba6c8a90f4 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:24:32 -0400 Subject: [PATCH 107/702] feat(libSceLibreSsl): implement the SSL and SSL_CTX ex_data index registry as LibreSSL 2.5.4 does SSL_get_ex_new_index and SSL_CTX_get_ex_new_index number their indices per class from 0, SSL_get_ex_data_X509_STORE_CTX_idx allocates its class-5 index once, and CRYPTO_cleanup_all_ex_data releases every class. An allocation failure returns -1 with ERR_R_MALLOC_FAILURE queued. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 10 ++- core/libs/prx/libSceLibreSsl/Export.cpp | 20 ------ core/libs/prx/libSceLibreSsl/include/Api.hpp | 6 +- core/libs/prx/libSceLibreSsl/src/ExData.cpp | 66 +++++++++++++++++++ core/libs/prx/libSceLibreSsl/tests/ExData.cpp | 54 +++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 1 + 6 files changed, 132 insertions(+), 25 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/src/ExData.cpp create mode 100644 core/libs/prx/libSceLibreSsl/tests/ExData.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 7a8ad2999c..f71e0ce841 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -2,7 +2,7 @@ add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/E target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) -target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Asn1.cpp) +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Asn1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ExData.cpp) if(BUILD_TESTING) add_test_executable(guest_libressl_x509_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslX509Errors.cpp) @@ -29,8 +29,14 @@ if(BUILD_TESTING) add_test(NAME guest_libressl_asn1 COMMAND guest_libressl_asn1_tests) add_test(NAME guest_libressl_asn1_allocator COMMAND guest_libressl_asn1_tests allocator) set_tests_properties(guest_libressl_asn1 guest_libressl_asn1_allocator PROPERTIES TIMEOUT 30) + add_test_executable(guest_libressl_ex_data_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/ExData.cpp) + target_include_directories(guest_libressl_ex_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_ex_data_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_ex_data_tests) + add_test(NAME guest_libressl_ex_data COMMAND guest_libressl_ex_data_tests) + set_tests_properties(guest_libressl_ex_data PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(guest_libressl_digest guest_libressl_asn1 guest_libressl_asn1_allocator PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_libressl_digest guest_libressl_asn1 guest_libressl_asn1_allocator guest_libressl_ex_data PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 93387ef0ba..8281476836 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -214,11 +214,6 @@ int APS5_VABI CRYPTO_THREADID_set_numeric_nid_postfix(void) { return 0; } -int APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI CRYPTO_num_locks_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1003,11 +998,6 @@ int APS5_VABI SSL_CTX_get_ex_data_nid_postfix(void) { return 0; } -int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_CTX_get_verify_mode_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1223,16 +1213,6 @@ int APS5_VABI SSL_get_ex_data_nid_postfix(void) { return 0; } -int APS5_VABI SSL_get_ex_data_X509_STORE_CTX_idx_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SSL_get_ex_new_index_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_get_fd_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index c7edd129f6..7e2bf6cad7 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -97,7 +97,7 @@ int APS5_VABI CONF_modules_free_nid_postfix(void); int APS5_VABI CONF_modules_load_file_nid_postfix(void); int APS5_VABI CRYPTO_THREADID_set_callback_nid_postfix(void); int APS5_VABI CRYPTO_THREADID_set_numeric_nid_postfix(void); -int APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void); +void APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void); int APS5_VABI CRYPTO_num_locks_nid_postfix(void); int APS5_VABI CRYPTO_set_add_lock_callback_nid_postfix(void); int APS5_VABI CRYPTO_set_locking_callback_nid_postfix(void); @@ -263,7 +263,7 @@ int APS5_VABI SSL_CTX_ctrl_nid_postfix(void); int APS5_VABI SSL_CTX_free_nid_postfix(void); int APS5_VABI SSL_CTX_get_cert_store_nid_postfix(void); int APS5_VABI SSL_CTX_get_ex_data_nid_postfix(void); -int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(void); +int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, void* newFunction, void* dupFunction, void* freeFunction); int APS5_VABI SSL_CTX_get_verify_mode_nid_postfix(void); int APS5_VABI SSL_CTX_load_verify_locations_nid_postfix(void); int APS5_VABI SSL_CTX_load_verify_mem_nid_postfix(void); @@ -308,7 +308,7 @@ int APS5_VABI SSL_get_current_cipher_nid_postfix(void); int APS5_VABI SSL_get_error_nid_postfix(void); int APS5_VABI SSL_get_ex_data_nid_postfix(void); int APS5_VABI SSL_get_ex_data_X509_STORE_CTX_idx_nid_postfix(void); -int APS5_VABI SSL_get_ex_new_index_nid_postfix(void); +int APS5_VABI SSL_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, void* newFunction, void* dupFunction, void* freeFunction); int APS5_VABI SSL_get_fd_nid_postfix(void); int APS5_VABI SSL_get_peer_cert_chain_nid_postfix(void); int APS5_VABI SSL_get_peer_certificate_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/ExData.cpp b/core/libs/prx/libSceLibreSsl/src/ExData.cpp new file mode 100644 index 0000000000..24a361df60 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/ExData.cpp @@ -0,0 +1,66 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libSceLibreSsl/include/Error.hpp" +#include +#include +#include +#include +#include + +namespace { +constexpr int CryptoLibrary = 15; +constexpr int AnyFunction = 0xfff; +constexpr int MallocFailure = 65; +constexpr int SslClass = 1; +constexpr int SslContextClass = 2; +constexpr int StoreContextClass = 5; +constexpr int ClassCount = 16; + +struct ExDataFunctions { + std::int64_t argl; + void* argp; + void* newFunction; + void* dupFunction; + void* freeFunction; +}; + +std::mutex classesLock; +std::array, ClassCount> classes; +std::mutex storeContextLock; +int storeContextIndex = -1; + +int NewIndex(int classIndex, const ExDataFunctions& functions) { + std::lock_guard lock(classesLock); + auto& registered = classes[classIndex]; + try { + registered.push_back(functions); + } catch (const std::bad_alloc&) { + LibreSslPutError_nid_no_patch(CryptoLibrary, AnyFunction, MallocFailure); + return -1; + } + return static_cast(registered.size() - 1); +} +} + +extern "C" { + +int APS5_VABI SSL_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, void* newFunction, void* dupFunction, void* freeFunction) { + return NewIndex(SslClass, {argl, argp, newFunction, dupFunction, freeFunction}); +} + +int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, void* newFunction, void* dupFunction, void* freeFunction) { + return NewIndex(SslContextClass, {argl, argp, newFunction, dupFunction, freeFunction}); +} + +int APS5_VABI SSL_get_ex_data_X509_STORE_CTX_idx_nid_postfix(void) { + static char verifyCallback[] = "SSL for verify callback"; + std::lock_guard lock(storeContextLock); + if (storeContextIndex < 0) storeContextIndex = NewIndex(StoreContextClass, {0, verifyCallback, nullptr, nullptr, nullptr}); + return storeContextIndex; +} + +void APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void) { + std::lock_guard lock(classesLock); + for (auto& registered : classes) std::vector().swap(registered); +} + +} diff --git a/core/libs/prx/libSceLibreSsl/tests/ExData.cpp b/core/libs/prx/libSceLibreSsl/tests/ExData.cpp new file mode 100644 index 0000000000..fec806bfed --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/tests/ExData.cpp @@ -0,0 +1,54 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +int NewSslIndex() { + return SSL_get_ex_new_index_nid_postfix(7, nullptr, nullptr, nullptr, nullptr); +} + +int NewContextIndex() { + int marker = 0; + return SSL_CTX_get_ex_new_index_nid_postfix(-1, &marker, nullptr, nullptr, nullptr); +} + +} + +int main() { + Require(NewSslIndex() == 0 && NewSslIndex() == 1 && NewSslIndex() == 2, "SSL indices count from 0"); + Require(NewContextIndex() == 0 && NewContextIndex() == 1, "SSL_CTX indices are counted apart from SSL ones"); + Require(SSL_get_ex_data_X509_STORE_CTX_idx_nid_postfix() == 0, "the verify-callback index is the first X509_STORE_CTX index"); + Require(SSL_get_ex_data_X509_STORE_CTX_idx_nid_postfix() == 0, "the verify-callback index is allocated once"); + Require(NewSslIndex() == 3, "the verify-callback index does not use an SSL index"); + + CRYPTO_cleanup_all_ex_data_nid_postfix(); + Require(NewSslIndex() == 0 && NewContextIndex() == 0, "cleanup releases every class"); + Require(SSL_get_ex_data_X509_STORE_CTX_idx_nid_postfix() == 0, "cleanup keeps the cached verify-callback index"); + CRYPTO_cleanup_all_ex_data_nid_postfix(); + CRYPTO_cleanup_all_ex_data_nid_postfix(); + Require(NewContextIndex() == 0, "cleanup can be repeated"); + + CRYPTO_cleanup_all_ex_data_nid_postfix(); + std::vector threads; + std::vector indices(64, -1); + for (int thread = 0; thread < 8; ++thread) + threads.emplace_back([&indices, thread] { + for (int round = 0; round < 8; ++round) indices[thread * 8 + round] = NewSslIndex(); + }); + for (auto& thread : threads) thread.join(); + std::vector seen(indices.size(), false); + for (const int index : indices) { + Require(index >= 0 && index < static_cast(indices.size()) && !seen[index], "concurrent callers get distinct indices"); + seen[index] = true; + } + return 0; +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index bd201c0765..fed54d8a36 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -10,6 +10,7 @@ - [AES_set_encrypt_key, AES_set_decrypt_key, AES_encrypt, AES_decrypt, AES_ecb_encrypt, AES_cbc_encrypt, AES_cfb128_encrypt, AES_ctr128_encrypt](../../../core/libs/prx/libSceLibreSsl/src/Aes.cpp) (libSceLibreSsl) - follow LibreSSL's [aes.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/aes/aes.h) and [mode code](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/modes). Nothing is checked on the console. Key setup follows [LibreSSL 2.5.4's aes_core.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/aes_core.c), the version the `X509_verify_cert_error_string` entry names: a null pointer returns -1 and a key length other than 128, 192 or 256 bits returns -2 before the key is written, and only the 44, 52 or 60 words the key length needs are written. The `AES_KEY` schedule uses that file's generic C layout (big-endian words, `rounds` 10, 12 or 14). The 2.5.4 amd64 build uses [aes-x86_64](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/asm/aes-x86_64.pl) instead, which copies the key bytes without swapping, so each word is byte-reversed compared with the C layout (OpenSSL 3.0.13's x86-64 code gives the same schedules byte-reversed); the console's build options are unknown, so a schedule the title reads or fills is unverified. CTR leaves the last keystream block in `ecount_buf` even at a block boundary. Null pointers in the cipher and mode functions, other `rounds` values and a CFB or CTR position outside 0-15 throw - [ERR_get_error, ERR_peek_error, ERR_clear_error, ERR_remove_thread_state](../../../core/libs/prx/libSceLibreSsl/src/Error.cpp) (libSceLibreSsl) - no export of this library adds an error yet, so the queue stays empty for guest code. The per-thread queue of 16 slots holding 15 codes, its order and the `ERR_PACK` layout follow LibreSSL's [err.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.c) and [err.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.h); the module's LibreSSL version is unknown and nothing is checked on the console. The queue keeps no file, line or data, as no export reads them. `ERR_remove_thread_state` with a thread id throws: that version asserts a null id, while older versions remove the state of the thread the id names - [OCSP_response_status_str, OCSP_cert_status_str and OCSP_crl_reason_str](../../../core/libs/prx/libSceLibreSsl/src/Ocsp.cpp) (libSceLibreSsl) - the module's strings are not measured on the console. The tables follow LibreSSL [ocsp_prn.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/ocsp/ocsp_prn.c), unchanged from revision 1.8 (2015) to 1.12 (2025), with codes from `ocsp.h`; the argument is a 64-bit `long` and every value not in a table returns `(UNKNOWN)` +- [SSL_get_ex_new_index, SSL_CTX_get_ex_new_index, SSL_get_ex_data_X509_STORE_CTX_idx and CRYPTO_cleanup_all_ex_data](../../../core/libs/prx/libSceLibreSsl/src/ExData.cpp) (libSceLibreSsl) - unverified on a PS5. They follow LibreSSL 2.5.4's [ex_data.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/ex_data.c#L310-L406), [ssl_lib.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_lib.c#L2828-L2854) and [ssl_cert.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_cert.c#L133-L160): each class (SSL 1, SSL_CTX 2, X509_STORE_CTX 5) numbers its indices from 0, the verify-callback index is allocated in class 5 once and kept even after `CRYPTO_cleanup_all_ex_data`, which releases every class so numbering restarts at 0, and an allocation failure returns -1 with `ERR_R_MALLOC_FAILURE` queued. The `argl`, `argp` and callbacks are stored but never called, because no export creates an `SSL` or `SSL_CTX` and `SSL_get_ex_data`/`SSL_set_ex_data` still throw. - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. From a45205c738f166c668074509048934dcac7a679b Mon Sep 17 00:00:00 2001 From: thepoppfish <147332153+thepoppfish@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:26:38 +0200 Subject: [PATCH 108/702] test(libc): cover concurrent stream status publication --- core/libs/prx/libc/CMakeLists.txt | 10 ++++++ core/libs/tests/FileStreamStatus.cpp | 51 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 core/libs/tests/FileStreamStatus.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index a307744beb..7b0cf60ea7 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -745,6 +745,16 @@ if(BUILD_TESTING) set_tests_properties(guest_gets PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(file_stream_status_tests ${LIBS_INCLUDE_DIR}/tests/FileStreamStatus.cpp) + target_include_directories(file_stream_status_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(file_stream_status_tests PRIVATE libc) + configure_windows_unwind(file_stream_status_tests) + add_test(NAME file_stream_status COMMAND file_stream_status_tests) + set_tests_properties(file_stream_status PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(file_stream_status PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(file_streams_tests ${LIBS_INCLUDE_DIR}/tests/FileStreams.cpp) target_include_directories(file_streams_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(file_streams_tests PRIVATE libc) diff --git a/core/libs/tests/FileStreamStatus.cpp b/core/libs/tests/FileStreamStatus.cpp new file mode 100644 index 0000000000..edf8b3efd8 --- /dev/null +++ b/core/libs/tests/FileStreamStatus.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/FileStream.hpp" +#include +#include +#include +#include +#include + +int main() { + auto* native = std::tmpfile(); + if (!native) return 1; + FileStream stream(native); + stream.MarkDirectory(); + constexpr std::int16_t unrelated = 0x1104; + stream.GuestState().flags = unrelated; + unsigned failures = 0; + for (const bool endOfFile : {false, true}) { + for (const bool error : {false, true}) { + stream.PublishStatus(endOfFile, error); + const auto flags = stream.GuestState().flags; + failures += flags != (unrelated | (endOfFile ? 0x20 : 0) | (error ? 0x40 : 0)); + errno = 123; + stream.KeepDirectoryReadError(false); + failures += errno != 123; + stream.KeepDirectoryReadError(true); + failures += errno != (error ? 21 : 123); + } + } + std::barrier start(2); + std::atomic invalid = 0; + std::thread publish([&] { + start.arrive_and_wait(); + for (unsigned index = 0; index < 20000; ++index) + stream.PublishStatus((index & 1) != 0, (index & 2) != 0); + }); + std::thread read([&] { + start.arrive_and_wait(); + for (unsigned index = 0; index < 20000; ++index) { + errno = 123; + stream.KeepDirectoryReadError(true); + if (errno != 123 && errno != 21) invalid.fetch_add(1, std::memory_order_relaxed); + const auto flags = __atomic_load_n(&stream.GuestState().flags, __ATOMIC_RELAXED); + if ((flags & ~0x60) != unrelated) invalid.fetch_add(1, std::memory_order_relaxed); + } + }); + publish.join(); + read.join(); + failures += invalid.load(std::memory_order_relaxed); + failures += std::fclose(native) != 0; + std::printf("file stream status: 4 serial cases, 20000 publications, 20000 reads, %u failures\n", failures); + return failures == 0 ? 0 : 1; +} From a7f91c6cd5a490cf850d0cc29fae593263f725a8 Mon Sep 17 00:00:00 2001 From: Seraph91P <8904984+Serph91P@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:36:11 +0000 Subject: [PATCH 109/702] fix(shader): select point samplers for direct Uint images Select the existing point-sampler variant for direct Uint image uses, matching the descriptor builder and the signed-integer path. Preserve the original sampler for float images sharing the same sampler. Add compute and draw regressions for shared float/integer samplers, including signed-integer and point-filtered controls. Keep Vulkan validation errors fatal so correct texel values cannot mask invalid linear sampling of integer formats. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++++ .../tests/execution/ImageTable.cpp | 65 +++++++++++++++++-- .../src/SpirvMemory/SpirvDescriptors.cpp | 2 +- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index cca520584e..661aad7f6b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2194,6 +2194,19 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() if(BUILD_TESTING) + foreach(sharedSamplerStage IN ITEMS dispatch draw) + foreach(sharedSamplerMode IN ITEMS uint sint point) + set(sharedSamplerTest agc_driver_shared_sampler_${sharedSamplerMode}_${sharedSamplerStage}) + add_test(NAME ${sharedSamplerTest} COMMAND agc_driver_image_table_tests --shared-sampler ${sharedSamplerMode} ${sharedSamplerStage}) + set_tests_properties(${sharedSamplerTest} PROPERTIES + TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + FAIL_REGULAR_EXPRESSION "SYNC-HAZARD-|VUID-|was not found but was requested by env var VK_INSTANCE_LAYERS") + if(WIN32) + set_tests_properties(${sharedSamplerTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() add_test(NAME agc_driver_address_passes_targets_split COMMAND agc_driver_address_passes_tests --targets) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1" SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 5ee8abca21..7d5f6a06e7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -1485,11 +1485,19 @@ constexpr std::uint32_t TargetWidth = 64; constexpr std::uint32_t TargetHeight = 32; alignas(256) std::array Pixels{}; alignas(256) std::array VertexResults{}; -alignas(256) std::array PixelResults{}; +alignas(256) std::array PixelResults{}; alignas(256) std::array GraphicsTable{}; 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}}}; -std::pair DrawTables(AgcDriver::VulkanDevice& device, std::uint32_t vertexKey, std::uint32_t pixelKey) { +alignas(256) constexpr std::array SharedSamplerCode{ + 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0xf4080900u, 0xfa000040u, 0xf4080a00u, 0xfa000050u, + 0x7e0202ffu, 0x3c400000u, 0x7e0402ffu, 0x3e000000u, + 0xf09c8f08u, 0x01090401u, 0xf09c8f08u, 0x010a0c01u, + 0x7e100280u, 0xe0781000u, 0x80070408u, 0xe0781010u, 0x80070c08u, + 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u, +}; + +std::pair DrawTables(AgcDriver::VulkanDevice& device, std::uint32_t vertexKey, std::uint32_t pixelKey, bool sharedSampler = false) { const auto target = device.Target(); Pixels.fill(std::byte{0}); VertexResults.fill(0xdeadbeefu); @@ -1524,9 +1532,15 @@ std::pair DrawTables(AgcDriver::VulkanDevice& devi std::copy(table.begin(), table.end(), pixelUserData.begin()); pixelUserData[4] = pixelKey; std::copy(pixelOutput.begin(), pixelOutput.end(), pixelUserData.begin() + 8); - const std::array pixelMemory{{{reinterpret_cast(PixelTableCode.data()), std::as_bytes(std::span(PixelTableCode))}, {reinterpret_cast(GraphicsTable.data()), std::as_bytes(std::span(GraphicsTable))}}}; + const std::span pixelCode = sharedSampler ? std::span(SharedSamplerCode) : std::span(PixelTableCode); + if (sharedSampler) { + const auto root = reinterpret_cast(Srt.data()); + pixelUserData = {static_cast(root), static_cast(root >> 32u)}; + std::copy(pixelOutput.begin(), pixelOutput.end(), Srt.begin() + 8); + } + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}, {reinterpret_cast(GraphicsTable.data()), std::as_bytes(std::span(GraphicsTable))}, {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}}}; ShaderRecompiler::RecompileRequest fragment{ - {ShaderStage::Fragment, reinterpret_cast(PixelTableCode.data()), PixelTableCode, 0, {}}, + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, PixelPushLayout(vertexPush, target) @@ -1557,6 +1571,41 @@ std::pair DrawTables(AgcDriver::VulkanDevice& devi return {vertexResult.PipelineVariantId(), pixelResult.PipelineVariantId()}; } +void RunSharedSamplerTests(AgcDriver::VulkanDevice& device, const std::string& mode, bool draw) { + Require(mode == "uint" || mode == "sint" || mode == "point", "unknown shared sampler mode"); + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto integerImage = mode == "sint" ? Image(SintTexels.data(), Format32SInt) : Image(UintTexels.data(), Format32UInt); + const auto& sampler = mode == "point" ? PointClamp : LinearWrap; + std::array images{}; + std::copy(integerImage.begin(), integerImage.end(), images.begin()); + std::copy(floatImage.begin(), floatImage.end(), images.begin() + 4); + std::copy(sampler.begin(), sampler.end(), Srt.begin() + 12); + std::copy(images.begin(), images.end(), Srt.begin() + 16); + const auto integer = mode == "sint" ? static_cast(SintTexels[0]) : UintTexels[0]; + const auto filtered = std::bit_cast(mode == "point" ? FloatTexels[0] : 0.5f); + if (draw) { + std::copy(floatImage.begin(), floatImage.end(), GraphicsTable.begin()); + DrawTables(device, 0u, 0u, true); + Require(PixelResults[0] == integer, "shared sampler draw: integer texel changed"); + Require(PixelResults[4] == filtered, "shared sampler draw: float filtering changed"); + } else { + alignas(256) constexpr std::array code{ + 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0xf4080900u, 0xfa000040u, 0xf4080a00u, 0xfa000050u, + 0x360200bfu, 0x7e020d01u, 0x060202ffu, 0x3f400000u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, + 0xf09c8f08u, 0x01090401u, 0xf09c8f08u, 0x010a0c01u, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, + 0xe0781000u, 0x80070408u, 0xe0781400u, 0x80070c08u, 0xbf810000u, + }; + const auto result = RunDirectCode(device, code, sampler, images); + Require(Output[0] == integer, "shared sampler dispatch: integer texel changed"); + Require(Output[256] == filtered, "shared sampler dispatch: float filtering changed"); + Require(Holds(result.samplers, sampler), "shared sampler dispatch: original sampler missing"); + Require(Holds(result.samplers, mode == "point" ? PointClamp : PointWrap), "shared sampler dispatch: point sampler missing"); + } + std::printf("shared sampler %s %s: integer=%u float_bits=%u passed\n", draw ? "draw" : "dispatch", mode.c_str(), integer, filtered); +} + void RunGraphicsTests(AgcDriver::VulkanDevice& device) { const auto floatImage = Image(FloatTexels.data(), Format32Float); const auto uintImage = Image(UintTexels.data(), Format32UInt); @@ -1576,7 +1625,7 @@ void RunGraphicsTests(AgcDriver::VulkanDevice& device) { } -int main() { +int main(int argc, char** argv) { try { #ifdef _WIN32 _putenv_s("APS5_SRGB_SHADER_DECODE", "1"); @@ -1585,6 +1634,12 @@ int main() { #endif const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 4 && std::string(argv[1]) == "--shared-sampler") { + Require(std::string(argv[3]) == "draw" || std::string(argv[3]) == "dispatch", "unknown shared sampler stage"); + RunSharedSamplerTests(*device, argv[2], std::string(argv[3]) == "draw"); + return 0; + } + Require(argc == 1, "unknown image table test arguments"); RunTests(*device); RunBorderSwizzleTests(*device); RunAnisoOverrideTests(*device); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 7a5fe34450..2e25aeabbe 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -187,7 +187,7 @@ std::uint32_t LoadSamplerDescriptor(SpirvEmitterState& state, std::uint32_t samp const auto& samplerResource = state.program.Info().samplers.at(sampler); const auto& image = state.runtimeImage; const bool table = samplerResource.table != NoTable || (image != nullptr && image->table != NoTable); - const bool point = image != nullptr && (table ? TablePointSampled(*image) : (image->numericClass == IrTextureNumericClass::Sint || image->conversionFormat != IrBufferFormat::Invalid || image->depthBits)); + const bool point = image != nullptr && (table ? TablePointSampled(*image) : (image->numericClass == IrTextureNumericClass::Uint || image->numericClass == IrTextureNumericClass::Sint || image->conversionFormat != IrBufferFormat::Invalid || image->depthBits)); const auto samplerType = state.module.Type(spv::OpTypeSampler); const auto pointerType = state.module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, samplerType); if (samplerResource.table != NoTable) { From e48781e8fe29a91e939f421672092a30ab366b34 Mon Sep 17 00:00:00 2001 From: ebosido444-ai Date: Mon, 26 Oct 2026 00:51:06 +0300 Subject: [PATCH 110/702] feat(relinker): report the build revision with --version The relinker binary carried no build identity: a release build and a checkout of main were indistinguishable, so a report could not name the build it came from. --version prints the revision git describe reports, falling back to "unknown" when the tree has no git metadata, and needs no input or output paths. The usage line and USAGE.md list the option. --- core/relinker/CMakeLists.txt | 9 +++++++++ core/relinker/cli/include/Cli.hpp | 1 + core/relinker/cli/src/CliArgs.cpp | 6 ++++-- core/relinker/cli/tests/test_help.py | 12 ++++++++++++ core/relinker/main.cpp | 7 +++++++ core/relinker/revision.cmake | 11 +++++++++++ docs/user/USAGE.md | 3 ++- 7 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 core/relinker/revision.cmake diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index b6dd553c3f..62c02fc7a6 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -152,6 +152,15 @@ target_link_libraries(relinker PRIVATE relinker_core) target_include_directories(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs) set_target_properties(relinker PROPERTIES CXX_EXTENSIONS OFF) +find_package(Git QUIET) +set(relinkerRevisionHeader ${CMAKE_CURRENT_BINARY_DIR}/RelinkerRevision.hpp) +add_custom_target(relinker_revision + COMMAND ${CMAKE_COMMAND} -DGIT=${GIT_EXECUTABLE} -DSOURCE=${CMAKE_SOURCE_DIR} -DOUTPUT=${relinkerRevisionHeader} -P ${CMAKE_CURRENT_SOURCE_DIR}/revision.cmake + BYPRODUCTS ${relinkerRevisionHeader} + VERBATIM) +add_dependencies(relinker relinker_revision) +target_include_directories(relinker PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + if(BUILD_TESTING AND WIN32) add_test_executable(windows_guest_export_alias_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/WindowsExportAliasTests.cpp) target_link_libraries(windows_guest_export_alias_tests PRIVATE relinker_core) diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 2625b44aac..71ee3f85d4 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -11,6 +11,7 @@ namespace Cli { struct Args { bool showHelp = false; + bool showVersion = false; bool skipSyscallCheck = false; bool trapSdkSyscallStubs = false; bool skipSceModule = false; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 6ab190a572..87a5d40f84 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -13,7 +13,7 @@ namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--linux-fixed-base
] [--macos-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [--frame-red-zone] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] [--patches | --no-patches] \n" + return "Usage: relinker [--help] [--version] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--linux-fixed-base
] [--macos-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [--frame-red-zone] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] [--patches | --no-patches] \n" " relinker --verify-patches [--verify-output] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... \n" "Example: relinker input.elf output.elf"; } @@ -59,6 +59,8 @@ Args ParseArgs(int argc, char* argv[]) { else if (arg.rfind("unused-filter=", 0) == 0) givenOptions.push_back("unused-filter="); if (arg == "--help") { args.showHelp = true; + } else if (arg == "--version") { + args.showVersion = true; } else if (arg == "--windows-fixed-base") { if (args.windowsFixedBase) throw std::runtime_error("--windows-fixed-base cannot be repeated"); if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) @@ -210,7 +212,7 @@ Args ParseArgs(int argc, char* argv[]) { if (args.macosFixedBase && !args.toMacos) throw std::runtime_error("--macos-fixed-base requires --macos"); - if (!args.showHelp && (args.inputPath.empty() || (!verifying && args.outputPath.empty()))) + if (!args.showHelp && !args.showVersion && (args.inputPath.empty() || (!verifying && args.outputPath.empty()))) throw std::runtime_error(Usage()); diff --git a/core/relinker/cli/tests/test_help.py b/core/relinker/cli/tests/test_help.py index 8c9659affc..4c8386180a 100644 --- a/core/relinker/cli/tests/test_help.py +++ b/core/relinker/cli/tests/test_help.py @@ -26,6 +26,18 @@ def main(): assert output.read_bytes() == b"preserve this output" assert sorted(path.name for path in work.iterdir()) == [output.name] + result = run("--version") + assert result.returncode == 0, result.stderr + assert result.stdout.startswith("relinker "), result.stdout + assert len(result.stdout.split()) == 2, result.stdout + assert result.stderr == "", result.stderr + assert output.read_bytes() == b"preserve this output" + assert sorted(path.name for path in work.iterdir()) == [output.name] + + result = run("--version", str(source), str(output)) + assert result.returncode == 0 and result.stdout.startswith("relinker "), result.stderr + assert output.read_bytes() == b"preserve this output" + result = run() assert result.returncode == 1 and "Usage: relinker" in result.stderr for args in [("--help", "--unknown"), ("--unknown", "--help")]: diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 81b6e3d359..13cefe2eb8 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -25,6 +25,8 @@ #include #endif +#include "RelinkerRevision.hpp" + namespace { struct ConvertedExecutable { @@ -264,6 +266,11 @@ int Run(const int argc, char* argv[]) { return 0; } + if (args.showVersion) { + std::cout << "relinker " << ANYPS5_RELINKER_REVISION << '\n'; + return 0; + } + Cli::ConsoleProgress progress(std::cout); try { if (!args.verifyPatchesPath.empty()) return RunVerifyPatches(args); diff --git a/core/relinker/revision.cmake b/core/relinker/revision.cmake new file mode 100644 index 0000000000..0abbecf613 --- /dev/null +++ b/core/relinker/revision.cmake @@ -0,0 +1,11 @@ +set(revision unknown) +if(GIT) + execute_process(COMMAND "${GIT}" -C "${SOURCE}" describe --tags --always --dirty + OUTPUT_VARIABLE described RESULT_VARIABLE failed OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(failed EQUAL 0 AND NOT described STREQUAL "") + set(revision ${described}) + endif() +endif() +file(WRITE "${OUTPUT}.new" "#pragma once\n#define ANYPS5_RELINKER_REVISION \"${revision}\"\n") +execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${OUTPUT}.new" "${OUTPUT}") +file(REMOVE "${OUTPUT}.new") diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index ff9a9227db..425fe71837 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -62,6 +62,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | Option | Effect | |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `--help` | Print usage to stdout and exit with code `0`; input and output paths are not required. | +| `--version` | Print the relinker build revision reported by `git describe` to stdout and exit with code `0`; input and output paths are not required. | | `--windows` | Produce a Windows PE executable. | | `--macos` | Produce an x86-64 Mach-O executable, with bundled modules as Mach-O libraries in `app0/sce_module`. Conflicts with `--windows`. | | `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | @@ -405,7 +406,7 @@ With `APS5_STALL_WATCHDOG_SECS=`, a queue that has waited `n` seconds without ## Exit codes -`0`: conversion succeeded or help was displayed. `1`: invalid arguments, or a `--verify-patches` entry that is a mismatch. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. +`0`: conversion succeeded, or help or the version was displayed. `1`: invalid arguments, or a `--verify-patches` entry that is a mismatch. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. ## Import audit From b7fa5adff4ee988135e15b040aee526be12728b4 Mon Sep 17 00:00:00 2001 From: Seraph91P <8904984+Serph91P@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:57:46 +0000 Subject: [PATCH 111/702] test(agc): integrate sampler regressions without argument conflicts --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 17 ++++------------- .../tests/execution/ImageTable.cpp | 11 ++++------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 661aad7f6b..71e25ce0f7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2194,19 +2194,6 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() if(BUILD_TESTING) - foreach(sharedSamplerStage IN ITEMS dispatch draw) - foreach(sharedSamplerMode IN ITEMS uint sint point) - set(sharedSamplerTest agc_driver_shared_sampler_${sharedSamplerMode}_${sharedSamplerStage}) - add_test(NAME ${sharedSamplerTest} COMMAND agc_driver_image_table_tests --shared-sampler ${sharedSamplerMode} ${sharedSamplerStage}) - set_tests_properties(${sharedSamplerTest} PROPERTIES - TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" - FAIL_REGULAR_EXPRESSION "SYNC-HAZARD-|VUID-|was not found but was requested by env var VK_INSTANCE_LAYERS") - if(WIN32) - set_tests_properties(${sharedSamplerTest} PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() - endforeach() - endforeach() add_test(NAME agc_driver_address_passes_targets_split COMMAND agc_driver_address_passes_tests --targets) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1" SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) @@ -2575,6 +2562,10 @@ if(BUILD_TESTING) set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME ${agcTestName} COMMAND ${agcTest}) set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(agcTestName STREQUAL "agc_driver_image_table") + set_tests_properties(${agcTestName} PROPERTIES TIMEOUT 30 + FAIL_REGULAR_EXPRESSION "SYNC-HAZARD-|VUID-|was not found but was requested by env var VK_INSTANCE_LAYERS") + endif() if(WIN32) set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 7d5f6a06e7..ed0f666935 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -1621,11 +1621,14 @@ void RunGraphicsTests(AgcDriver::VulkanDevice& device) { for (std::uint32_t vertex = 0; vertex < 3u; ++vertex) Require(VertexResults[vertex * 4u] == static_cast(SintTexels[0]), "image table graphics: the vertex stage read the wrong entry after the keys changed"); Require(PixelResults[0] == std::bit_cast(FloatTexels[0]), "image table graphics: the pixel stage read the wrong entry after the keys changed"); Require(first == second, "image table graphics: other table keys built another pipeline"); + for (const auto* mode : {"uint", "sint", "point"}) { + for (const bool draw : {false, true}) RunSharedSamplerTests(device, mode, draw); + } } } -int main(int argc, char** argv) { +int main() { try { #ifdef _WIN32 _putenv_s("APS5_SRGB_SHADER_DECODE", "1"); @@ -1634,12 +1637,6 @@ int main(int argc, char** argv) { #endif const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (argc == 4 && std::string(argv[1]) == "--shared-sampler") { - Require(std::string(argv[3]) == "draw" || std::string(argv[3]) == "dispatch", "unknown shared sampler stage"); - RunSharedSamplerTests(*device, argv[2], std::string(argv[3]) == "draw"); - return 0; - } - Require(argc == 1, "unknown image table test arguments"); RunTests(*device); RunBorderSwizzleTests(*device); RunAnisoOverrideTests(*device); From 3a4e9492a52576feda8610916f939000db035343 Mon Sep 17 00:00:00 2001 From: ebosido444-ai Date: Mon, 26 Oct 2026 00:54:12 +0300 Subject: [PATCH 112/702] fix(web): report download progress for the system libraries Every conversion starts with a multi-MB libraries download, but the worker only sent the phase, so the bar stayed indeterminate and the percent stayed blank while the transfer ran: it read as a hang on a slow connection. Send completed and total bytes from the download loop; the existing 80 ms throttle keeps the messages bounded. A blocked or offline connection died as a bare "Failed to fetch" during the release lookup. Name the failure and the fix instead. --- demo/web/app.mjs | 2 +- demo/web/worker.mjs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/demo/web/app.mjs b/demo/web/app.mjs index 56e33dabf0..b83dd23b8f 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -333,7 +333,7 @@ if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!value.tag || !["windows", "linux"].every(target => Number.isSafeInteger(value.platforms?.[target]?.expandedBytes))) throw new Error("Invalid release information"); release = value; update(); -}).catch(error => fail(error.message)); +}).catch(error => fail(error instanceof TypeError ? "Cannot reach the release information: check the connection and reload the page." : error.message)); if (!("webkitdirectory" in $("folder"))) { $("choose-folder").disabled = true; diff --git a/demo/web/worker.mjs b/demo/web/worker.mjs index 51d63e03e0..be35844090 100644 --- a/demo/web/worker.mjs +++ b/demo/web/worker.mjs @@ -57,6 +57,7 @@ export async function downloadLibraries(target) { if (done) break; size += value.byteLength; if (size > asset.size) throw new Error("System library download is incomplete"); + report({ phase: "Downloading system libraries", completed: size, total: asset.size }); chunks.push(value); } if (size !== asset.size) throw new Error("System library download is incomplete"); @@ -194,7 +195,7 @@ self.onmessage = async ({ data }) => { summaries = []; } else throw new Error("Unknown worker request"); } catch (error) { - self.postMessage({ type: "error", message: error instanceof Error ? error.message : String(error) }); + self.postMessage({ type: "error", message: error instanceof TypeError ? "Cannot reach the release files: check the connection and try again." : error instanceof Error ? error.message : String(error) }); } finally { busy = false; } From 9e7edf6e0ca53ac01b0067e1e2ba27dd4928d725 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 14:33:49 +1300 Subject: [PATCH 113/702] fix(agc): do not evict host imports used in the current or previous frame An import was evictable 256 lookups after its last use. A frame makes thousands of lookups, so when the imports a frame uses do not all fit the budget, each new one evicted another of the same frame and the whole set was imported again every frame. A flip packet now ends a frame, and only imports not used in the current or the previous frame are evicted; what does not fit is refused and copied, as below the budget. A frame boundary also expires the remembered evicting refusals. --- .../src/Driver/Packets/PacketExecution.cpp | 2 + .../src/Driver/Queues/Submission.cpp | 2 + .../Graphics/include/GuestBufferMemory.hpp | 2 + .../Graphics/src/GuestBufferMemory.cpp | 24 +++++--- .../libSceAgcDriver/tests/BdaResources.cpp | 57 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 5972698258..b7a4622cae 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -10,6 +10,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include @@ -406,6 +407,7 @@ void Driver::execute(const Submission& submission) { } if (!flipInterruptDeferred) deliverFlipEopInterrupt(submission.queue); ++flipsCounted; + Graphics::NoteHostImportFrame(); if (batchesAtFlip != 0) flipSerial = batchesAtFlip; flipBatchesUnsignaled += unsignaledAtFlip; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index eeaea3e126..b1b48ac743 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -7,6 +7,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/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp" #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" @@ -440,6 +441,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) { ++flipsCounted; submission.flips.begin()->second->GpuReady(frame); lock.unlock(); + Graphics::NoteHostImportFrame(); AgcDriverDeliverEopInterrupt(0); return; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index bc6afff426..99a5a3274b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -117,6 +117,8 @@ void SetHostImportDeferredSettles(bool enabled); std::uint64_t HostImportBudget(const VkPhysicalDeviceMemoryProperties& memory, std::uint64_t physicalBytes); void SetHostImportMaxBytes(std::uint64_t bytes); std::uint64_t HostImportLiveBytes(); +// A flip ends a frame: an import used in the current or the previous frame is never evicted for another one. +void NoteHostImportFrame(); std::uint64_t HostImportBudgetRemaining(const Context& context); struct HostImportSettleCounts { std::uint64_t submitted = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 24f1fc3e13..0b37c2e454 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -274,6 +274,8 @@ struct HostImports { #endif std::uint64_t useCounter = 0; std::uint64_t buildFloor = 0; + std::uint64_t frameFloor = 0; + std::uint64_t previousFrameFloor = 0; std::array counts{}; VkPhysicalDevice reportedDevice = VK_NULL_HANDLE; std::chrono::steady_clock::time_point reportedAt{}; @@ -1483,10 +1485,8 @@ bool pointerHeld(const HostImports& state, const HostImport& entry) { return effectiveUse(state, entry) >= state.buildFloor; } -constexpr std::uint64_t ProtectedUses = 256; - bool evictable(const HostImports& state, const HostImport& entry) { - return !pointerHeld(state, entry) && effectiveUse(state, entry) + ProtectedUses < state.useCounter; + return !pointerHeld(state, entry) && effectiveUse(state, entry) < state.previousFrameFloor; } bool evictingRefusalCurrent(const HostImports& state, const EvictingRefusal& refusal, std::uint64_t budget) { @@ -1661,8 +1661,9 @@ const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std: const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end); const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict, ImportRequester requester); -// The bytes retireLeastRecent could free outside [begin, end). -std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end, std::uint64_t& ready, std::uint64_t& readyHeld) { +// The bytes retireLeastRecent could free outside [begin, end). readyHeld is 0 when an import a build still holds is already +// two frames old: it becomes evictable as soon as that build's floor rises, which ends a remembered refusal. +std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end, std::uint64_t& readyHeld) { std::uint64_t bytes = 0; for (const auto& [base, entry] : state.imports) { if (entry.base + entry.bytes > begin && entry.base < end) continue; @@ -1670,8 +1671,7 @@ std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std: bytes += entry.pinnedBytes; continue; } - auto& horizon = pointerHeld(state, entry) ? readyHeld : ready; - horizon = std::min(horizon, effectiveUse(state, entry) + ProtectedUses + 1); + if (pointerHeld(state, entry) && effectiveUse(state, entry) < state.previousFrameFloor) readyHeld = 0; } return bytes; } @@ -1861,7 +1861,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import const auto live = state.liveBytes - replaced; auto ready = std::numeric_limits::max(); auto readyHeld = std::numeric_limits::max(); - const auto evictable = evict && live + bytes > budget ? evictableBytes(state, base, base + bytes, ready, readyHeld) : 0; + const auto evictable = evict && live + bytes > budget ? evictableBytes(state, base, base + bytes, readyHeld) : 0; if (live + bytes > budget + evictable) { // A refused import turns every later use of the range into CPU copies, so say so. static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); @@ -3725,6 +3725,14 @@ void SetHostImportMaxBytes(std::uint64_t bytes) { importMaxBytes.store(bytes, std::memory_order_relaxed); } +void NoteHostImportFrame() { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + state.previousFrameFloor = state.frameFloor; + state.frameFloor = state.useCounter + 1; + ++state.epoch; +} + void SetHostImportBudget(std::uint64_t bytes) { budgetOverride.store(bytes, std::memory_order_relaxed); auto& state = Imports(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 37e518eb51..d6452dcefd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -685,6 +685,8 @@ void importCoverageHintBudgetTests(const Context& context, const BdaTestAccess& Require(first != nullptr && first->base == a && first->bytes == ranges.bytes, "coverage budget fixture did not import its first range"); const auto firstBuffer = first->buffer; const auto firstSerial = HostImportSerial(ranges.importing, a, 16, false); + NoteHostImportFrame(); + NoteHostImportFrame(); for (int use = 0; use < 300; ++use) Require(HostImportFor(ranges.importing, d, 16) != nullptr, "coverage budget fixture did not import its hot range"); const auto hotSerial = HostImportSerial(ranges.importing, d, 16, false); Require(firstSerial != 0 && hotSerial != 0 && HostImportLiveBytes() == 2 * ranges.bytes, "coverage budget fixture has unexpected import ownership or usage"); @@ -732,6 +734,8 @@ void failedImportJoinProbeBudgetTests(const Context& context, const BdaTestAcces const auto* first = HostImportFor(ranges.importing, a, 16); Require(first != nullptr && first->base == a && first->bytes == ranges.bytes, "failed join budget fixture did not import its first range"); const auto firstSerial = HostImportSerial(ranges.importing, a, 16, false); + NoteHostImportFrame(); + NoteHostImportFrame(); for (int use = 0; use < 300; ++use) Require(HostImportFor(ranges.importing, d, 16) != nullptr, "failed join budget fixture did not import its hot range"); const auto hotSerial = HostImportSerial(ranges.importing, d, 16, false); Require(firstSerial != 0 && hotSerial != 0 && HostImportLiveBytes() == 2 * ranges.bytes, "failed join budget fixture has unexpected import ownership or usage"); @@ -1108,6 +1112,8 @@ void importAtMapOverBudgetTests(const Context& context) { leased.Upload(false); leased.WriteBack(); } + NoteHostImportFrame(); + NoteHostImportFrame(); const auto live = HostImportLiveBytes(); SetHostImportBudget(live + bytes - 1); change(mapped, true); @@ -1127,6 +1133,52 @@ void importAtMapOverBudgetTests(const Context& context) { for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes}); } +// Three ranges under a budget for two, each used by a build of its own in every frame, with more lookups between the +// uses than a frame's worth of imports. The two imported first keep their imports and the third is refused at every +// frame, instead of evicting one of the others (which then evicts the next, every frame). A range unused for two frames +// is evicted for the third. +void importEvictionFramesTests(const Context& context) { + constexpr std::size_t bytes = 65536; + auto importing = context; + importing.hostImportAlignment = bytes; + std::array blocks{}; + std::array at{}; + const auto change = [&](void* block, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < blocks.size(); ++index) { + blocks[index] = ::operator new(bytes, std::align_val_t{bytes}); + at[index] = reinterpret_cast(blocks[index]); + change(blocks[index], true); + } + SetHostImportBudget(HostImportLiveBytes() + 2 * bytes); + const auto use = [&](std::size_t index) { + GuestBufferMemory leased(importing); + leased.AddReadable(at[index], 32); + leased.Upload(false); + leased.WriteBack(); + for (int lookup = 0; lookup < 300; ++lookup) static_cast(HostImportFor(importing, at[index], 16)); + return HostImportSerial(importing, at[index], 16, false); + }; + std::array serials{use(0), use(1)}; + Require(serials[0] != 0 && serials[1] != 0, "two ranges under a budget for two were not imported"); + for (int frame = 0; frame < 10; ++frame) { + Require(use(2) == 0, "a range over the budget evicted an import used in the same frame (frame " + std::to_string(frame) + ")"); + NoteHostImportFrame(); + Require(use(0) == serials[0] && use(1) == serials[1], "an import used in every frame was imported again (frame " + std::to_string(frame) + ")"); + } + NoteHostImportFrame(); + Require(use(1) == serials[1] && use(2) == 0, "a range over the budget evicted an import used in the previous frame"); + NoteHostImportFrame(); + Require(use(1) == serials[1] && use(2) != 0 && !HostImportCovers(importing, at[0], 16), "an import unused for two frames was not evicted for a range over the budget"); + SetHostImportBudget(0); + for (auto* block : blocks) change(block, false); + for (auto* block : blocks) ::operator delete(block, std::align_val_t{bytes}); +} + void importRequesterTests(const Context& context, const BdaTestAccess& access) { ClearHostImports(context.device); constexpr std::size_t piece = 65536; @@ -1601,6 +1653,8 @@ void evictingRefusalMemoryTests(const Context& context) { } before = counted(); Require(HostImportFor(importing, address(filled + 2), 16) == nullptr && counted().remembered == before.remembered + 1, "a remembered evicting refusal ended when a build raised the floor while no import could age out of its protection yet"); + NoteHostImportFrame(); + NoteHostImportFrame(); for (int use = 0; use < 200; ++use) Require(HostImportFor(importing, address(filled + 1), 16) != nullptr, "a live import was not found"); before = counted(); Require(HostImportFor(importing, address(filled + 2), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after the imports aged out of their protection"); @@ -1637,6 +1691,8 @@ void evictingRefusalAgingTests(const Context& context) { static_cast(HostImportFor(importing, address(filled), 16)); auto before = counted(); Require(HostImportFor(importing, address(filled), 16) == nullptr && counted().remembered == before.remembered + 1, "a refusal while the imports were recent but no longer held by a build was not remembered"); + NoteHostImportFrame(); + NoteHostImportFrame(); for (int use = 0; use < 200; ++use) Require(HostImportFor(importing, address(filled - 1), 16) != nullptr, "a live import was not found"); before = counted(); Require(HostImportFor(importing, address(filled), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after imports no build held aged out of their protection"); @@ -4223,6 +4279,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importReportFloorTests(context, access, false); importReportRefusalMemoryTests(context, access); sparseImportedHeapTests(context, access); + importEvictionFramesTests(context); heapMirrorTests(context, access); partialHeapImportBudgetTests(context, access); importedHeapMirrorTests(context, access); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7dfbf66ac5..c6f20c73be 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -247,7 +247,7 @@ - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. -- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. +- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the current or the previous frame (a frame ends at a flip packet or a CPU flip), so a set used in every frame keeps its imports and what does not fit is copied, as below the budget; without flips (a loading stretch, compute-only work) no frame ends and nothing is evictable, so imports that do not fit are copied instead of evicting by recency; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: no frame has ended, and no build holding an import two frames old has raised its floor. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the two-frame protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. - [Piece-split ranges over the host import budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): an address-based build imports a registered range larger than the device's largest allocation in pieces. If the budget refuses a piece, the range is mirrored or copied without the evicting retry other refused ranges get when their copy would pass `APS5_ADDRESS_COPY_MAX_MIB`, so such a copy can still exceed that cap and end the build. - [Device-local staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written or atomic buffer element inside a host import, including one an address-based compute build reaches through its pointers, is copied to device memory before the dispatch and copied back whole after it, so a CPU store into that range in between is rolled back. - [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. From 4580ed63016d647bbb032a49bb6dbeffdd01bfb0 Mon Sep 17 00:00:00 2001 From: krishalivaibhav Date: Mon, 26 Oct 2026 13:38:53 +0530 Subject: [PATCH 114/702] fix(libSceLibcInternal): declare environ for the spawn test on macOS Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestPosixSpawn.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index d71e652950..04a204b538 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -21,7 +21,7 @@ extern "C" int kill(int, int); #endif -#if defined(__linux__) +#ifndef _WIN32 extern char** environ; #endif From 3fe0cd9b8bf425b6f2b1127ac501f1f46b009b35 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Mon, 26 Oct 2026 01:24:46 +0100 Subject: [PATCH 115/702] test(agc): give the storage image budget test a device-local heap as large as its reported budget agc_driver_sampled_cache_budget_memory reports a 64 GiB budget on the largest device-local heap but kept the heap's real size. On an APU that heap is 512 MiB, so the storage image cache ceiling is the 512 MiB floor, and both checks failed: half of the heap or of the budget (256 MiB) and the budget under pressure (1024 MiB). The test now raises the heap it reports to at least the reported budget, so its expectations hold whatever heap the device has. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 6cfb074620..2edc80d0ac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -1051,6 +1051,7 @@ void storageMemoryTests(Context context) { auto held = Sampled(context, block.Surface(0)); const auto heap = LargestDeviceLocalHeap(context.memory); Require(heap.has_value(), "the device has no device-local heap"); + context.memory.memoryHeaps[*heap].size = std::max(context.memory.memoryHeaps[*heap].size, ReportedBudget); const auto half = std::min(context.memory.memoryHeaps[*heap].size / 2u, ReportedBudget / 2u); Require(ReadStorageBudget(context) == half, "52 GiB free of a 64 GiB budget does not leave the storage image cache half of the heap or of the budget"); context.memoryProperties2 = &ReportPressure; From 5f10d0a2296c27ff46a0c9645c5d7b1f2c72f893 Mon Sep 17 00:00:00 2001 From: QRMOE <217609482+QRMOE@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:19:10 +0100 Subject: [PATCH 116/702] fix(shader): keep pixel helper lanes out of guest lane masks Pixel-stage ballots and EXEC reads count only lanes that are not helper invocations, so a waterfall loop over a saved whole-quad EXEC picks and clears live lanes only. In programs that execute s_wqm on EXEC, writing EXEC from a value keeps a helper invocation active while a live lane of its quad is, so implicit-LOD sampling after a save/restore still has its helpers. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/PixelHelperWaterfall.cpp | 201 ++++++++++++++++++ .../src/SpirvMemory/SpirvSubgroup.cpp | 7 + .../Translation/TranslationContext.hpp | 3 + .../src/ControlFlowInstructions.cpp | 6 +- .../Translation/src/InstructionTranslator.cpp | 6 + .../src/TranslationContext/ExecMask.cpp | 28 ++- .../src/TranslationContext/OperandAccess.cpp | 4 +- docs/dev/debt/shader.md | 2 +- 8 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperWaterfall.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperWaterfall.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperWaterfall.cpp new file mode 100644 index 0000000000..11d741c17a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperWaterfall.cpp @@ -0,0 +1,201 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Levels = 5; +constexpr std::uint32_t PassLimit = 80; +alignas(256) std::array Pixels{}; +alignas(256) std::array Results{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, 0x000000c0, 0xbec4047e, 0x7e120506, 0xbefe0a7e, + 0x101400ff, 0x3baaaaab, 0x101602ff, 0x3c000000, 0xf0800f08, 0x01060c0a, 0xbe8a047e, 0xbe8c0380, + 0xbe8d0380, 0xbe940480, 0xbe82140a, 0xd7600003, 0x00000506, 0x890d030d, 0xd4c20014, 0x00020c03, + 0x8a8a140a, 0x800c810c, 0xbf09ff0c, 0x00000050, 0xbf850002, 0xbf13800a, 0xbf85fff3, 0xbefe0444, + 0x8f0c900c, 0x880d0c0d, 0x7e100209, 0x7e12020d, 0xe0742000, 0x80010806, 0xbf8c3f70, 0xf800180f, + 0x0f0e0d0c, 0xbf810000, +}; + +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}; +} + +std::array TextureDescriptor() { + const auto address = reinterpret_cast(Texels.data()); + return {static_cast(address >> 8), static_cast((address >> 40) & 0xffu) | (56u << 20) | (3u << 30), + ((Size - 1u) >> 2) | ((Size - 1u) << 14), 0xfacu | ((Levels - 1u) << 16) | (9u << 28), 0u, (Levels - 1u) << 4, 0u, 0u}; +} + +void FillTexture() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor())); + Require(geometry.guestBytes <= Texels.size(), "helper waterfall texture does not fit"); + Texels.fill(0xffu); +} + +std::vector> Triangles() { + std::vector> vertices; + constexpr std::uint32_t columns = 6; + constexpr std::uint32_t rows = 4; + for (std::uint32_t cell = 0; cell < columns * rows; ++cell) { + const float width = 2.0f / columns; + const float height = 2.0f / rows; + const float x = -1.0f + width * static_cast(cell % columns); + const float y = -1.0f + height * static_cast(cell / columns); + const float skew = 0.07f * static_cast(cell % 5u); + vertices.push_back({x + (0.05f + skew) * width, y + 0.05f * height, 0.5f, 1.0f}); + vertices.push_back({x + 0.93f * width, y + (0.11f + skew) * height, 0.5f, 1.0f}); + vertices.push_back({x + (0.37f - skew * 0.5f) * width, y + 0.91f * height, 0.5f, 1.0f}); + } + return vertices; +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) { + static const auto triangles = Triangles(); + Pixels.fill(std::byte{0}); + Results.fill(Sentinel); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast(triangles.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()); + + 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); + std::vector pixelUserData(36, 0u); + const auto results = BufferDescriptor(Results.data(), 8u, static_cast(Results.size() / 2u)); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const auto texture = TextureDescriptor(); + std::copy(texture.begin(), texture.end(), pixelUserData.begin() + 24); + const std::array sampler{0u, 0xfffu << 12, 1u << 26, 0u}; + std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 32); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(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)} + }}; + + 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.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(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize) { + const auto what = "pixel helper waterfall wave" + std::to_string(waveSize); + std::map> waves; + std::uint32_t partialQuads = 0; + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + if (Pixels[pixel * 4u + 3u] == std::byte{0}) { + Require(Results[pixel * 2u] == Sentinel, what + ": uncovered pixel " + std::to_string(pixel) + " stored a result"); + continue; + } + Require(Results[pixel * 2u] != Sentinel, what + ": covered pixel " + std::to_string(pixel) + " stored nothing"); + waves[Results[pixel * 2u]].push_back(pixel); + const auto quad = (pixel / Width & ~1u) * Width + (pixel % Width & ~1u); + if (quad == pixel) { + for (const auto offset : {1u, Width, Width + 1u}) { + if (Pixels[(quad + offset) * 4u + 3u] == std::byte{0}) { + ++partialQuads; + break; + } + } + } + } + Require(waves.size() > 4u && partialQuads > 16u, what + ": the triangles made only " + std::to_string(waves.size()) + " waves and " + std::to_string(partialQuads) + " partly covered quads"); + for (const auto& [first, live] : waves) { + std::uint32_t keys = 0; + for (const auto pixel : live) keys ^= pixel; + const auto passes = static_cast(live.size()); + Require(passes < PassLimit, what + ": the wave whose first lane is pixel " + std::to_string(first) + " has more live lanes than the pass limit"); + for (const auto pixel : live) { + const auto stored = Results[pixel * 2u + 1u]; + Require((stored >> 16u) == passes, what + ": pixel " + std::to_string(pixel) + " of the wave whose first lane is pixel " + std::to_string(first) + " made " + std::to_string(stored >> 16u) + " passes over " + std::to_string(passes) + " live lanes"); + Require((stored & 0xffffu) == keys, what + ": pixel " + std::to_string(pixel) + " of the wave whose first lane is pixel " + std::to_string(first) + " read keys " + std::to_string(stored & 0xffffu) + ", expected " + std::to_string(keys)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + FillTexture(); + for (const auto waveSize : {64u, 32u}) { + for (std::uint32_t pass = 0; pass < 4; ++pass) { + Draw(*device, waveSize, device->Target()); + Check(waveSize); + } + } + std::puts("pixel helper waterfall tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index 4c0c787f07..6348414594 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp @@ -100,6 +100,13 @@ namespace ShaderRecompiler state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), result, own, zero, zero, zero); return; } + if (state.program.Resources().stage == IrShaderStage::Pixel) { + if (const auto helperVariable = InputVariableForKind(state, StageInputKind::HelperInvocation); helperVariable != 0u) { + const auto helper = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeBool(state), helper, helperVariable); + predicate = Binary(state, spv::OpLogicalAnd, TypeBool(state), predicate, Unary(state, spv::OpLogicalNot, TypeBool(state), helper)); + } + } state.module.AddSubgroupBallot(TypeU32Vector(state, 4u), result, ConstantU32(state, spv::ScopeSubgroup), predicate); } diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 00da5a7db5..d22b83ae2d 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -25,6 +25,7 @@ public: } void SetEntryRounding(const RoundingState& state) { setRounding(state); } void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; } + void SetWholeQuad(bool entered) { wholeQuad = entered; } void SetFloat32DenormPreserve(bool enabled) { float32DenormPreserve = enabled; } void SetFloat32FlushToZero(bool enabled) { float32FlushToZero = enabled; } void AddBranchCondition(const BasicBlock& source, BlockInfo& info); @@ -57,6 +58,7 @@ private: IrU32 applyBitSourceModifiers(const RdnaOperand& operand, IrU32 value); IrValue* readOperand(const RdnaOperand& operand, IrType type); IrU1 threadBit(const std::array& mask); + IrU1 execLanes(const std::array& mask); void writeRawU32(const RdnaOperand& operand, IrU32 value); IrF32 applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value); IrF32 scaleF32Result(std::uint32_t omod, IrF32 value); @@ -382,6 +384,7 @@ private: RoundingState rounding; bool ieeeMode = false; bool nativeFloat16 = false; + bool wholeQuad = false; IrBuilder ir; IrBlock& block; IrU1 instructionBranchCondition; diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index bb8094339a..da01650ed5 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -45,7 +45,7 @@ void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin) writeRawU32(inst.destination, newCounter); ir.SetExecLo(newExecLo.Value()); ir.SetExecHi(newExecHi.Value()); - ir.SetExec(threadBit({newExecLo, newExecHi}).Value()); + ir.SetExec(execLanes({newExecLo, newExecHi}).Value()); } // s_*_saveexec reads its source before it writes the old exec into the destination, so @@ -84,7 +84,7 @@ void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operati if (writeResult) writeU32Pair(inst.destination, {hostExecWord(0u), hostExecWord(1u)}); const IrU1 nonZero(ir.LogicalOr(ir.INotEqual(newExecLo.Value(), ir.Constant(0u)), ir.INotEqual(newExecHi.Value(), ir.Constant(0u)))); ir.SetScc(nonZero.Value()); - ir.SetExec(threadBit({newExecLo, newExecHi}).Value()); + ir.SetExec(execLanes({newExecLo, newExecHi}).Value()); return; } const IrU32 oldExec(ir.GetExecLo()); @@ -116,7 +116,7 @@ void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operati IrValue& combinedLane = ir.Emit(laneOperation, IrType::U1, {&execLane, &sourceLaneOperand}); newLane = IrU1(negateResult ? ir.LogicalNot(combinedLane) : combinedLane); } else { - newLane = threadBit({newExec, IrU32(ir.GetExecHi())}); + newLane = execLanes({newExec, IrU32(ir.GetExecHi())}); } if (writeResult) writeSaved(hostExecWord(0u), newLane); const IrU1 nonZero(ir.INotEqual(newExec.Value(), ir.Constant(0u))); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 08393a3942..d93ba5507e 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -493,6 +493,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con program.SetWaveSize(options.waveSize); std::uint32_t windowBase = NumVectorRegs; bool indexedWrites = false; + bool wholeQuad = false; for (const auto& cfgBlock : cfg.blocks) { for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { const auto& instruction = decoded.instructions[index]; @@ -511,6 +512,10 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con windowBase = std::min({windowBase, instruction.source0.reg, instruction.destination.reg}); indexedWrites = true; break; + case RdnaOpcode::SWqmB32: + case RdnaOpcode::SWqmB64: + wholeQuad = wholeQuad || instruction.destination.kind == RdnaOperandKind::ExecLo || instruction.destination.kind == RdnaOperandKind::ExecHi; + break; default: break; } @@ -599,6 +604,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con context.SetPixelInput(options.inputInfo.pixel, options.fragmentShaderBarycentricEnabled); context.SetFloatMode(options.floatMode); context.SetNativeFloat16(options.nativeFloat16); + context.SetWholeQuad(wholeQuad); context.SetFloat32DenormPreserve(options.float32DenormPreserve); context.SetFloat32FlushToZero(options.float32FlushToZero); context.SetEntryRounding(entryRounding.at(cfgBlock.id)); diff --git a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp index 5a87cd02e4..bc26b8dbb7 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp @@ -15,14 +15,32 @@ IrU32 TranslationContext::hostExecWord(std::uint32_t half) { if (half != 0u && program.WaveSize() != 64u) { return word; } - auto active = ballotMask(IrU1(ir.ConstantBool(true))); - if (program.Resources().stage == IrShaderStage::Pixel) { - const IrU64 lanes(ir.ConstructU64(active[0].Value(), active[1].Value())); - active = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&lanes.Value()}))); - } + const auto active = ballotMask(IrU1(ir.ConstantBool(true))); return IrU32(ir.BitwiseAnd(word.Value(), active[half].Value())); } +IrU1 TranslationContext::execLanes(const std::array& mask) { + if (program.Resources().stage != IrShaderStage::Pixel || !wholeQuad) return threadBit(mask); + if (mask[0].Value().HasImmediate() && mask[1].Value().HasImmediate()) { + const std::uint64_t value = (static_cast(mask[1].Value().ImmediateU32()) << 32u) | mask[0].Value().ImmediateU32(); + std::uint64_t quads = value | (value >> 1u) | (value >> 2u) | (value >> 3u); + quads &= 0x1111111111111111ull; + if (quads * 0xfull == value) return threadBit(mask); + } + IrValue& builtin = ir.Emit(IrOpcode::GetBuiltin, IrOpcodeType(IrOpcode::GetBuiltin), {&ir.Constant(static_cast(StageInputKind::HelperInvocation)), &ir.Constant(0u)}); + const auto live = ballotMask(IrU1(ir.IEqual(builtin, ir.Constant(0u)))); + const IrU64 liveLanes(ir.ConstructU64(live[0].Value(), live[1].Value())); + const auto liveQuads = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&liveLanes.Value()}))); + const IrU64 lanes(ir.ConstructU64(mask[0].Value(), mask[1].Value())); + const auto quads = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&lanes.Value()}))); + std::array words = mask; + for (std::size_t half = 0; half < words.size(); ++half) { + IrValue& helpers = ir.BitwiseAnd(liveQuads[half].Value(), ir.BitwiseNot(live[half].Value())); + words[half] = IrU32(ir.BitwiseOr(mask[half].Value(), ir.BitwiseAnd(quads[half].Value(), helpers))); + } + return threadBit(words); +} + IrU1 TranslationContext::threadBit(const std::array& mask) { const bool wide = program.WaveSize() == 64u; if (mask[0].Value().HasImmediate() && (!wide || mask[1].Value().HasImmediate())) { diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index ebec2355da..2f07f9846c 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -567,11 +567,11 @@ void TranslationContext::writeRawU32(const RdnaOperand& operand, IrU32 value) { case RdnaOperandKind::M0: ir.SetM0(value.Value()); break; case RdnaOperandKind::ExecLo: ir.SetExecLo(value.Value()); - ir.SetExec(threadBit({value, IrU32(ir.GetExecHi())}).Value()); + ir.SetExec(execLanes({value, IrU32(ir.GetExecHi())}).Value()); break; case RdnaOperandKind::ExecHi: ir.SetExecHi(value.Value()); - ir.SetExec(threadBit({IrU32(ir.GetExecLo()), value}).Value()); + ir.SetExec(execLanes({IrU32(ir.GetExecLo()), value}).Value()); break; case RdnaOperandKind::Scc: ir.SetScc(ir.INotEqual(value.Value(), ir.Constant(0u))); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d8ca31f348..7ed7c75214 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -100,7 +100,7 @@ - [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from PPSA04263's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. -- [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) is limited to host lanes. Pixel reads retain helper bits in quads with an active fragment, preserving whole-quad masks across copies. Quad expansion follows the [RDNA 2 ISA, section 12.3, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119); save/restore followed by implicit-LOD sampling is checked on an RX 6700 XT, not on a console. The console also holds lanes the host lacks, e.g. 32-63 of a wave64 on 32 host lanes. +- [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) and every [pixel-stage ballot](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) leave out helper invocations, unlike the console: guest SGPR masks, lane reads and lane counts never include a helper lane, so a waterfall over helper lanes skips them (not measured on a console). Vulkan lets helpers stay out of non-quad subgroup operations, and on an RX 9070 XT they ran no ordinary instructions, so a helper bit in a saved whole-quad EXEC could never be cleared. In a pixel program that runs `s_wqm` on EXEC, writing EXEC from a value keeps a helper active while a live lane of its quad is, for implicit-LOD sampling ([RDNA 2 ISA, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119)). EXEC reads also stay within host lanes; the console holds lanes the host lacks (32-63 of a wave64 on 32 host lanes). - [`v_readlane_b32`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. - 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) or, in a program whose entry EXEC is every lane (vertex, local and tessellation stages), as 0 when the value is 0 in lanes 32-63; other such reads throw. A value is 0 there when it is built from 0 through selects under a lane index (lane id or `v_mbcnt` of -1) below at most 32, `v_cmp_ne_u32` against 0, OR, DPP and permlanes; the counts in `s3` give such a bound on a 32-lane host. On a 32-lane host a half-wave scan may also run under `s_mov_b64 exec, -1`, use bound_ctrl:1 steps for an OR, add or unsigned max, and take keys that are 0 in lanes 32-63; its lane 31 is then the subgroup reduction of the host's invocations (a shuffle of the scan on a device without subgroup arithmetic): idle invocations add the identity, where the console would add the keys of the lanes `s3` claims. From 61ba654e904dad093c5ac27a5397382c3a486dc0 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 26 Oct 2026 04:34:30 -0400 Subject: [PATCH 117/702] feat(agc): read selected live images without guest writeback --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 ++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 70 ++++++++++++++++++- .../tests/execution/LayeredVolumeDraw.cpp | 44 ++++++++++++ docs/dev/ENVIRONMENT.md | 1 + 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 521a3f93bd..a7438467df 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2885,6 +2885,13 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_volume_direct_readback COMMAND agc_driver_layered_volume_draw_tests) + set_tests_properties(agc_driver_volume_direct_readback PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_TEST_DIRECT_READBACK=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + if(WIN32) + set_tests_properties(agc_driver_volume_direct_readback PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_f16_to_f32_unpack_spirv COMMAND agc_driver_f16_to_f32_unpack_tests --verify-spirv) set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ada06eab1e..2c4ab1c24f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -20,6 +20,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include #include #include #include @@ -4497,11 +4498,78 @@ bool StorageDumpSelects(const GuestTextureResource& descriptor) { return selector.width != 0 && descriptor.width == selector.width && descriptor.height == selector.height && descriptor.depthOrLastArray + 1 == selector.layers && (selector.anyFormat || descriptor.format == selector.format); } +bool StorageDumpReady() { + const char* gate = std::getenv("APS5_DUMP_STORAGE_GATE"); + std::error_code error; + return gate == nullptr || std::filesystem::exists(gate, error); +} + } void StorageTexture::FlushDumpSelected(const char* reason) { const auto& selector = StorageDumpSelection(); if (selector.address == 0 && selector.width == 0) return; + if (!StorageDumpReady()) return; + if (std::getenv("APS5_DUMP_STORAGE_GATE") != nullptr) { + std::lock_guard gpu(GuestMemory::GpuMutex()); + static std::vector captured; + std::vector> selected; + { + auto& live = Live(); + std::lock_guard lock(live.mutex); + for (auto* texture : live.textures) { + if (selected.size() + captured.size() >= 8) break; + if (!StorageDumpSelects(texture->descriptor) || texture->sliceLinearBytes * texture->arrayLayers > (8u << 20u)) continue; + if (std::find(captured.begin(), captured.end(), texture->descriptor.baseAddress) != captured.end()) continue; + if (auto alive = texture->weak_from_this().lock()) selected.push_back(std::move(alive)); + } + } + if (!selected.empty()) { + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) recorder->Submit(); + for (const auto& texture : selected) { + const auto& context = texture->context; + Check(context.Function("vkDeviceWaitIdle")(context.device), "private image readback wait"); + Buffer host(context, static_cast(texture->sliceLinearBytes * texture->arrayLayers), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + CommandBatch batch(context); + const auto commands = batch.Handle(); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = texture->image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, texture->descriptor.mipCount, 0, texture->geometry.imageLayers}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + const auto regions = texture->CopyRegions(); + context.Function("vkCmdCopyImageToBuffer")(commands, texture->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, host.Handle(), static_cast(regions.size()), regions.data()); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + const auto hostBarrier = WholeBufferBarrier(host.Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &hostBarrier, 1, &barrier); + batch.SubmitAndWait(); + char name[80]; + std::snprintf(name, sizeof(name), "storage_direct_%llx_%zu.raw", static_cast(texture->descriptor.baseAddress), captured.size()); + std::FILE* file = std::fopen(name, "wb"); + Require(file != nullptr, "cannot open private image readback file"); + const auto& mip = texture->mips[0]; + const std::uint32_t header[3] = {mip.pitchBytes / static_cast(BytesPerElement(texture->descriptor.format)), mip.height, static_cast(texture->storageFormat)}; + bool written = std::fwrite(header, sizeof(header), 1, file) == 1; + for (std::uint32_t layer = 0; layer < texture->arrayLayers; ++layer) { + if (!texture->geometry.HasLayer(0, layer)) continue; + written = std::fwrite(host.Bytes().data() + texture->geometry.LinearLayerOffset(layer) + mip.linearOffset, 1, static_cast(mip.linearSize), file) == mip.linearSize && written; + } + const bool closed = std::fclose(file) == 0; + Require(written && closed, "cannot complete private image readback file"); + captured.push_back(texture->descriptor.baseAddress); + std::fprintf(stderr, "[storage-dump] direct=%s bytes=%llu\n", name, static_cast(mip.linearSize * texture->arrayLayers)); + } + } + return; + } std::vector> ranges; { auto& pending = Pending(); @@ -6274,7 +6342,7 @@ 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}); } } - const bool dumpSelected = StorageDumpSelects(descriptor); + const bool dumpSelected = StorageDumpSelects(descriptor) && StorageDumpReady(); static int dumps = 0; std::unique_ptr dump; if (dumpSelected && dumps < 8) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp index 5581d46dcc..5777b04e35 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -269,6 +270,17 @@ void Check(const std::byte* volume, const AgcDriver::Graphics::ColorTarget& colo int main(int argc, char** argv) { try { + const bool directReadback = std::getenv("APS5_TEST_DIRECT_READBACK") != nullptr; + if (directReadback) { + std::remove("private-readback.ready"); + char extent[64]; + std::snprintf(extent, sizeof(extent), "%ux%ux%u", Width, Height, Slices); +#ifdef _WIN32 + Require(_putenv_s("APS5_DUMP_STORAGE", extent) == 0 && _putenv_s("APS5_DUMP_STORAGE_GATE", "private-readback.ready") == 0, "cannot set private readback options"); +#else + Require(setenv("APS5_DUMP_STORAGE", extent, 1) == 0 && setenv("APS5_DUMP_STORAGE_GATE", "private-readback.ready", 1) == 0, "cannot set private readback options"); +#endif + } const bool vertexOnly = argc == 2 && std::strcmp(argv[1], "--vertex-only") == 0; Require(argc == 1 || vertexOnly, "usage: layered volume draw [--vertex-only]"); const auto device = OpenVulkanTestDevice(); @@ -283,6 +295,38 @@ int main(int argc, char** argv) { const auto color = VolumeTarget(volume.Data()); Run(*device, color, false, PixelCode, false); Check(volume.Data(), color, "vertex draw", WhiteBands); + if (directReadback) { + char name[80]; + std::snprintf(name, sizeof(name), "storage_direct_%llx_0.raw", static_cast(color.address)); + std::remove(name); + AgcDriver::Graphics::StorageTexture::FlushDumpSelected("closed readback gate fixture"); + Require(!std::filesystem::exists(name), "private readback ignored its closed gate"); + auto* trigger = std::fopen("private-readback.ready", "wb"); + Require(trigger != nullptr, "cannot create private readback trigger"); + Require(std::fclose(trigger) == 0, "cannot close private readback trigger"); + AgcDriver::Graphics::StorageTexture::FlushDumpSelected("private readback fixture"); + std::remove("private-readback.ready"); + auto* file = std::fopen(name, "rb"); + Require(file != nullptr, "clean volume image was not exported by direct readback"); + std::array header{}; + Require(std::fread(header.data(), sizeof(header), 1, file) == 1, "private readback header is incomplete"); + Require(header[0] >= Width && header[1] == Height && header[2] == VK_FORMAT_R8G8B8A8_UNORM, "private readback header disagrees with the volume"); + std::vector pixels(static_cast(header[0]) * Height * Slices * 4u); + Require(std::fread(pixels.data(), 1, pixels.size(), file) == pixels.size(), "private readback pixels are incomplete"); + Require(std::fgetc(file) == EOF, "private readback has unexpected trailing pixels"); + Require(std::fclose(file) == 0, "cannot close private readback"); + for (std::uint32_t z = 0; z < Slices; ++z) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool drawn = z >= FirstSlice && x / BandWidth == z - FirstSlice; + const auto expected = drawn ? WhiteBands[z - FirstSlice] : Kept; + for (std::uint32_t channel = 0; channel < 4; ++channel) Require(pixels[((z * Height + y) * header[0] + x) * 4u + channel] == expected, "private readback changed a volume texel"); + } + } + } + std::puts("private clean-volume direct readback pixels verified"); + return 0; + } const GuestVolume fieldVolume((VolumeTarget(nullptr).bytes + 65535u) & ~std::size_t{65535u}); const auto fieldColor = VolumeTarget(fieldVolume.Data()); Run(*device, fieldColor, false, LayerFieldPixelCode, true); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d54b9c8fd1..d09544e984 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -143,6 +143,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DUMP_SCALE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 4 | keeps every n-th pixel in each direction of a dumped frame | | `APS5_DUMP_SHADERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | writes each prepared shader's request to shader_
.req | | `APS5_DUMP_STORAGE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | selects storage textures to dump by hex address or WxHxL[:format] extent as storage_
_.raw | +| `APS5_DUMP_STORAGE_GATE` | path | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | with `APS5_DUMP_STORAGE`, waits for this path to exist and reads matching live images directly without guest writeback; at most eight addresses, 8 MiB linear storage per image; writes `storage_direct_
_.raw` with the existing pitch, height and VkFormat header and first-mip pixels | | `APS5_DUMP_SUBMITS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp` | | directory in which every submission is captured for replay | | `APS5_DUMP_TARGETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | 0 | dumps the first n renders of each non-resident color target as target_
_.raw in the working directory; a non-zero value also runs every draw synchronously, and in practice it needs APS5_NO_RESIDENT_TARGETS=1 | | `APS5_DUMP_TEXTURE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | hex addresses or WxH extents whose sampled textures are dumped on first upload as texture_
_.raw | From a208275b3adf292654c1e4f96a29ed26ed381740 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 26 Oct 2026 04:48:30 -0400 Subject: [PATCH 118/702] docs(agc): list the direct readback regression switch --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d09544e984..b7c4b4bf76 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -144,6 +144,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DUMP_SHADERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | writes each prepared shader's request to shader_
.req | | `APS5_DUMP_STORAGE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | selects storage textures to dump by hex address or WxHxL[:format] extent as storage_
_.raw | | `APS5_DUMP_STORAGE_GATE` | path | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | with `APS5_DUMP_STORAGE`, waits for this path to exist and reads matching live images directly without guest writeback; at most eight addresses, 8 MiB linear storage per image; writes `storage_direct_
_.raw` with the existing pitch, height and VkFormat header and first-mip pixels | +| `APS5_TEST_DIRECT_READBACK` | set | `core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp` | | runs the clean-volume gated live-image readback regression in the layered volume execution test | | `APS5_DUMP_SUBMITS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp` | | directory in which every submission is captured for replay | | `APS5_DUMP_TARGETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | 0 | dumps the first n renders of each non-resident color target as target_
_.raw in the working directory; a non-zero value also runs every draw synchronously, and in practice it needs APS5_NO_RESIDENT_TARGETS=1 | | `APS5_DUMP_TEXTURE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | hex addresses or WxH extents whose sampled textures are dumped on first upload as texture_
_.raw | From 2d3b71067ff44c4c96fb1675378066367eef6869 Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Mon, 26 Oct 2026 14:21:18 +0530 Subject: [PATCH 119/702] style(agc): drop the macOS-only in Resources.cpp main's Resources.cpp already uses std::byte in Buffer::Bytes() on every platform without including ; it arrives through the project headers this file includes first. Guarding a second include under __APPLE__ suggested otherwise, so drop it. Also keep the blank line inside the __APPLE__ block so the text outside it stays identical to main. --- core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index 8dbb767364..25b19ef6df 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -7,7 +7,6 @@ #include #include #ifdef __APPLE__ -#include #include #endif From 415e302c9cd74f20bd56f8942a4e1d972c23e9f3 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:36:51 +0000 Subject: [PATCH 120/702] perf(agc): probe the f32 denormal flush on NVIDIA's proprietary driver The driver declares no shaderDenormFlushToZeroFloat32, so every f32 add, mul and fma kept the software flush. Run the same probe as on MoltenVK and use the driver's default handling when it matches the emulated flush. --- .../libSceAgcDriver/Execution/include/Float32FlushControl.hpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp | 3 ++- docs/dev/ARCHITECTURE.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp index 0d93c2c2a4..3e6af23dcb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp @@ -12,7 +12,7 @@ struct Float32FlushControl { inline Float32FlushControl Float32FlushControlFor(VkDriverId driver, VkBool32 flushToZero, VkShaderFloatControlsIndependence independence) { const bool declared = flushToZero == VK_TRUE && independence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE; - return {declared || driver == VK_DRIVER_ID_MOLTENVK, declared}; + return {declared || driver == VK_DRIVER_ID_MOLTENVK || driver == VK_DRIVER_ID_NVIDIA_PROPRIETARY, declared}; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp index f51d28a2bf..55dc9f5ba9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp @@ -17,7 +17,8 @@ constexpr Case Cases[]{ {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, - {VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, false, false}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, false}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, {VK_DRIVER_ID_AMD_PROPRIETARY, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, {static_cast(0), VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, }; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index e63cb8cf20..d9cb0b072e 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -210,4 +210,4 @@ The register layout and instruction contract follow [AMD RDNA2 ISA sections 5.8 ### Hardware f32 denormal flush -A device with `shaderDenormFlushToZeroFloat32` and an f32 denormal behaviour independent of the other widths runs a probe when it is created (`ProbeFloat32FlushToZero`): the three programs of `agc_driver_f32_denormal_flush` (one mixed module with every flushing instruction, and the tiny-product programs), with no float mode, with `IEEE_MODE` off and with it on, each compiled twice, with the emulated flush and with `DenormFlushToZero 32` declared. When every result matches, `SpirvTarget::float32FlushToZero` is set: every module declares the mode, and `v_add_f32`, `v_sub_f32`, `v_subrev_f32`, `v_mul_f32`, the fused `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32` and `v_fmaak_f32`, `v_floor_f32` and `v_ceil_f32` drop their software flush in the default rounding mode. VOP3P forms, the other rounding modes and the other instructions keep the emulation. `APS5_NO_HW_F32_FLUSH` skips the probe. +A device with `shaderDenormFlushToZeroFloat32` and an f32 denormal behaviour independent of the other widths, and MoltenVK and NVIDIA's proprietary driver, which declare no f32 flush, run a probe when the device is created (`ProbeFloat32FlushToZero`): the three programs of `agc_driver_f32_denormal_flush` (one mixed module with every flushing instruction, and the tiny-product programs), with no float mode, with `IEEE_MODE` off and with it on, each compiled twice, with the emulated flush and with `DenormFlushToZero 32` declared (on a device that declares no f32 flush, with no execution mode, so the driver's default f32 denormal handling is what is measured). When every result matches, `SpirvTarget::float32FlushToZero` is set: every module declares the mode where the device has it, and `v_add_f32`, `v_sub_f32`, `v_subrev_f32`, `v_mul_f32`, the fused `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32` and `v_fmaak_f32`, `v_floor_f32` and `v_ceil_f32` drop their software flush in the default rounding mode. VOP3P forms, the other rounding modes and the other instructions keep the emulation. `APS5_NO_HW_F32_FLUSH` skips the probe. From 3e7a0ae671dc03912fadd1b58cd383ba34121a20 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:45:10 +0000 Subject: [PATCH 121/702] fix(shader): keep the f32 floor and ceil flush where the device flushes by default NVIDIA's proprietary driver flushes f32 add, multiply and fma denormals by default but not the inputs of floor and ceil, so its startup probe failed only on those two. Drop their software flush only where the device declares DenormFlushToZero 32. --- core/shader/recompiler/Recompiler.cpp | 1 + .../Translation/include/Translation/InstructionTranslator.hpp | 1 + .../Translation/include/Translation/TranslationContext.hpp | 2 ++ .../recompiler/Translation/src/InstructionTranslator.cpp | 1 + core/shader/recompiler/Translation/src/VectorInstructions.cpp | 4 ++-- docs/dev/ARCHITECTURE.md | 2 +- 6 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 5d4ecc297a..dffd30ea20 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -245,6 +245,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat translateOptions.interpolationFunctions = supports(spv::CapabilityInterpolationFunction); translateOptions.floatMode = request.context.floatMode; translateOptions.float32FlushToZero = HardwareFloat32Flush(request); + translateOptions.float32FlushDeclared = supports(spv::CapabilityDenormFlushToZero); translateOptions.inputInfo = inputInfo; translateOptions.float32DenormPreserve = request.target.float32DenormPreserve; diff --git a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp index 21f6bc7e0e..b72d41b050 100644 --- a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp +++ b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp @@ -36,6 +36,7 @@ struct TranslateOptions { bool interpolationFunctions = false; bool nativeFloat16 = false; bool float32FlushToZero = false; + bool float32FlushDeclared = false; bool float32DenormPreserve = false; bool gcnLegacyVertexInputs = false; std::optional floatMode; diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 00da5a7db5..f51e2f016b 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -27,6 +27,7 @@ public: void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; } void SetFloat32DenormPreserve(bool enabled) { float32DenormPreserve = enabled; } void SetFloat32FlushToZero(bool enabled) { float32FlushToZero = enabled; } + void SetFloat32FlushDeclared(bool enabled) { float32FlushDeclared = enabled; } void AddBranchCondition(const BasicBlock& source, BlockInfo& info); void TranslateCodeTableLoad(const RdnaInstruction& instruction, const ControlFlowGraph::CodeTableLoad& table); void TranslateUnlinkedCall(const RdnaInstruction& instruction); @@ -388,6 +389,7 @@ private: RdnaOpcode currentOpcode = RdnaOpcode::Unknown; std::uint32_t f32DenormalFlush = 0u; bool float32FlushToZero = false; + bool float32FlushDeclared = false; bool float32DenormPreserve = false; std::uint32_t currentProgramCounter = 0; std::uint32_t currentVectorLimit = 1; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 08393a3942..b8e1f33338 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -601,6 +601,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con context.SetNativeFloat16(options.nativeFloat16); context.SetFloat32DenormPreserve(options.float32DenormPreserve); context.SetFloat32FlushToZero(options.float32FlushToZero); + context.SetFloat32FlushDeclared(options.float32FlushDeclared); context.SetEntryRounding(entryRounding.at(cfgBlock.id)); for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { const auto& instruction = decoded.instructions[index]; diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 804535419f..20a45cee03 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -30,9 +30,9 @@ bool TranslationContext::flushesInHardware(const RdnaInstruction& inst) const { case RdnaOpcode::VAddF32: case RdnaOpcode::VSubF32: case RdnaOpcode::VSubrevF32: - case RdnaOpcode::VMulF32: + case RdnaOpcode::VMulF32: return true; case RdnaOpcode::VFloorF32: - case RdnaOpcode::VCeilF32: return true; + case RdnaOpcode::VCeilF32: return float32FlushDeclared; case RdnaOpcode::VFmaF32: case RdnaOpcode::VMacF32: case RdnaOpcode::VMadmkF32: diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index d9cb0b072e..60c5ed0221 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -210,4 +210,4 @@ The register layout and instruction contract follow [AMD RDNA2 ISA sections 5.8 ### Hardware f32 denormal flush -A device with `shaderDenormFlushToZeroFloat32` and an f32 denormal behaviour independent of the other widths, and MoltenVK and NVIDIA's proprietary driver, which declare no f32 flush, run a probe when the device is created (`ProbeFloat32FlushToZero`): the three programs of `agc_driver_f32_denormal_flush` (one mixed module with every flushing instruction, and the tiny-product programs), with no float mode, with `IEEE_MODE` off and with it on, each compiled twice, with the emulated flush and with `DenormFlushToZero 32` declared (on a device that declares no f32 flush, with no execution mode, so the driver's default f32 denormal handling is what is measured). When every result matches, `SpirvTarget::float32FlushToZero` is set: every module declares the mode where the device has it, and `v_add_f32`, `v_sub_f32`, `v_subrev_f32`, `v_mul_f32`, the fused `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32` and `v_fmaak_f32`, `v_floor_f32` and `v_ceil_f32` drop their software flush in the default rounding mode. VOP3P forms, the other rounding modes and the other instructions keep the emulation. `APS5_NO_HW_F32_FLUSH` skips the probe. +A device with `shaderDenormFlushToZeroFloat32` and an f32 denormal behaviour independent of the other widths, and MoltenVK and NVIDIA's proprietary driver, which declare no f32 flush, run a probe when the device is created (`ProbeFloat32FlushToZero`): the three programs of `agc_driver_f32_denormal_flush` (one mixed module with every flushing instruction, and the tiny-product programs), with no float mode, with `IEEE_MODE` off and with it on, each compiled twice, with the emulated flush and with `DenormFlushToZero 32` declared (on a device that declares no f32 flush, with no execution mode, so the driver's default f32 denormal handling is what is measured). When every result matches, `SpirvTarget::float32FlushToZero` is set: every module declares the mode where the device has it, and `v_add_f32`, `v_sub_f32`, `v_subrev_f32`, `v_mul_f32`, the fused `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32` and `v_fmaak_f32` drop their software flush in the default rounding mode, and `v_floor_f32` and `v_ceil_f32` drop it only where the device declares the mode (NVIDIA's default handling does not flush their inputs). VOP3P forms, the other rounding modes and the other instructions keep the emulation. `APS5_NO_HW_F32_FLUSH` skips the probe. From 1fbf3a42d2d6f6e6e0b73c19a9f1e8a3eec12fe6 Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Sun, 25 Oct 2026 23:03:58 +0000 Subject: [PATCH 122/702] fix(libkernel): fail the signal action, mask, stack and pending calls with EFAULT on an unusable pointer FreeBSD 11.4's sys_sigaction, sys_sigprocmask and sys_sigaltstack copy their input in before validating it and copy the old value out after the change, and sys_sigpending copies out; a bad pointer gives EFAULT. The guest pointers were dereferenced directly. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/General.cpp | 7 ++++ .../libs/prx/libkernel/System/src/Signals.cpp | 42 ++++++++++++++----- core/libs/tests/GuestSignals.cpp | 41 ++++++++++++++++++ 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 4d4db4d068..17d627e327 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -31,6 +31,7 @@ extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& desti extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, std::size_t bytes); +extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, std::size_t bytes); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); extern "C" std::filesystem::path ResolveDiskPath_nid_no_patch(const char* path); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index f905c3ff30..cfd71da184 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -837,6 +837,13 @@ extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, #endif } +extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + const auto start = reinterpret_cast(source); + if (start == 0 || bytes > UINTPTR_MAX - start) return false; + return CopyGuestBytes(destination, source, bytes); +} + extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { auto resolved = ResolveLocked(path, false, true); AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 091f1db7d4..acab049df9 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -542,7 +542,7 @@ thread_local GuestStack alternateStack{nullptr, 0, SsDisable}; namespace { -int ChangeBlockedMask(int how, const GuestSignalSet* set, GuestSignalSet* previousSet, bool releaseHolds, bool* caught = nullptr) { +int ChangeBlockedMask(int how, const GuestSignalSet* set, GuestSignalSet* previousSet, bool releaseHolds, bool* caught = nullptr, GuestSignalSet* guestPrevious = nullptr) { if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } { std::lock_guard lock(registration); @@ -576,8 +576,10 @@ int ChangeBlockedMask(int how, const GuestSignalSet* set, GuestSignalSet* previo } } } + const bool faulted = guestPrevious != nullptr && !GuestCopyOut_nid_no_patch(guestPrevious, previousSet, sizeof(*previousSet)); std::atomic_thread_fence(std::memory_order_seq_cst); if (DeliverUnblockedPending() && caught != nullptr) *caught = true; + if (faulted) { *__error_nid_postfix() = 14; return -1; } return 0; } @@ -603,6 +605,11 @@ GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { return previous; } int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, GuestSigaction* previous) { + GuestSigaction requested{}; + if (action != nullptr) { + if (!GuestCopyIn_nid_no_patch(&requested, action, sizeof(requested))) { *__error_nid_postfix() = 14; return -1; } + action = &requested; + } if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } if (action && action->handler > 1 && (action->flags & ~SaDefinedFlags) != 0) { *__error_nid_postfix() = 22; return -1; } if (action && (guest == GuestSigkill || guest == GuestSigstop) && action->handler != 0) { *__error_nid_postfix() = 22; return -1; } @@ -614,7 +621,7 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue dispositions[guest] = *action; DiscardPendingIfIgnored(guest, *action); } - if (previous) *previous = old; + if (previous && !GuestCopyOut_nid_no_patch(previous, &old, sizeof(old))) { *__error_nid_postfix() = 14; return -1; } return 0; } int APS5_VABI _sigaction_nid_postfix(int guest, const GuestSigaction* action, GuestSigaction* previous) { @@ -664,18 +671,27 @@ int APS5_VABI pthread_kill_nid_postfix(Pthread thread, int guest) { } int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previous) { GuestStack replacement = alternateStack; + GuestStack requested{}; if (stack != nullptr) { + if (!GuestCopyIn_nid_no_patch(&requested, stack, sizeof(requested))) { *__error_nid_postfix() = 14; return -1; } + stack = &requested; if ((stack->flags & ~SsDisable) != 0) { *__error_nid_postfix() = 22; return -1; } if (stack->flags & SsDisable) replacement.flags = SsDisable; else if (stack->size < MinSignalStackSize) { *__error_nid_postfix() = 12; return -1; } else replacement = {stack->sp, stack->size, 0}; } - if (previous != nullptr) *previous = alternateStack; + const GuestStack old = alternateStack; alternateStack = replacement; + if (previous != nullptr && !GuestCopyOut_nid_no_patch(previous, &old, sizeof(old))) { *__error_nid_postfix() = 14; return -1; } return 0; } int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { - return ChangeBlockedMask(how, set, previousSet, true); + GuestSignalSet requested{}, old{}; + if (set != nullptr) { + if (!GuestCopyIn_nid_no_patch(&requested, set, sizeof(requested))) { *__error_nid_postfix() = 14; return -1; } + set = &requested; + } + return ChangeBlockedMask(how, set, previousSet != nullptr ? &old : nullptr, true, nullptr, previousSet); } int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet) { @@ -684,8 +700,10 @@ int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSe } int APS5_VABI pthread_sigmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { - if (set != nullptr && (how < 1 || how > 3)) return 22; - return _sigprocmask_nid_postfix(how, set, previousSet); + const int saved = *__error_nid_postfix(); + const int error = _sigprocmask_nid_postfix(how, set, previousSet) == 0 ? 0 : *__error_nid_postfix(); + *__error_nid_postfix() = saved; + return error; } int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet* set, void* info, const KernelTimespec* timeout) { @@ -745,10 +763,14 @@ int APS5_VABI _sigsuspend_nid_postfix(const GuestSignalSet* set) { int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { if (set == nullptr) { *__error_nid_postfix() = 14; return -1; } - auto& pending = Pending(); - std::lock_guard lock(pending.lock); - for (int word = 0; word < 4; ++word) - set->bits[word] = pending.words[word].load(std::memory_order_relaxed) & BlockedWord(word); + GuestSignalSet result{}; + { + auto& pending = Pending(); + std::lock_guard lock(pending.lock); + for (int word = 0; word < 4; ++word) + result.bits[word] = pending.words[word].load(std::memory_order_relaxed) & BlockedWord(word); + } + if (!GuestCopyOut_nid_no_patch(set, &result, sizeof(result))) { *__error_nid_postfix() = 14; return -1; } return 0; } } diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 8afec0ead8..fe96c136d1 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -66,8 +66,49 @@ void APS5_VABI MaskCallback(int value) { } static void Require(bool value) { if (!value) std::abort(); } +static void CheckSignalPointerFaults() { + constexpr std::size_t page = 0x4000; + void* memory = nullptr; + Require(sceKernelMapFlexibleMemory(&memory, page, 3, 0) == 0 && memory != nullptr); + Require(sceKernelMprotect(memory, page, 1) == 0); + const auto readOnly = memory; + const auto unreadable = reinterpret_cast(std::uintptr_t{0x10}); + const auto fails = [](int result) { return result == -1 && *__error_nid_postfix() == 14; }; + constexpr int user = 30; + GuestSigaction previous{}; + Require(fails(sigaction_nid_postfix(user, static_cast(unreadable), nullptr))); + Require(fails(sigaction_nid_postfix(0, static_cast(unreadable), nullptr))); + Require(fails(sigaction_nid_postfix(user, nullptr, static_cast(unreadable)))); + Require(fails(sigaction_nid_postfix(user, nullptr, static_cast(readOnly)))); + Require(sigaction_nid_postfix(0, nullptr, &previous) == -1 && *__error_nid_postfix() == 22); + Require(sigaction_nid_postfix(user, nullptr, &previous) == 0); + GuestSignalSet blocked{}, set{{1u << (user - 1), 0, 0, 0}}, none{}; + Require(sigprocmask_nid_postfix(3, nullptr, &blocked) == 0); + Require(fails(sigprocmask_nid_postfix(1, unreadable, nullptr))); + Require(fails(sigprocmask_nid_postfix(9, unreadable, nullptr))); + Require(sigprocmask_nid_postfix(9, &set, nullptr) == -1 && *__error_nid_postfix() == 22); + Require(fails(sigprocmask_nid_postfix(1, nullptr, unreadable))); + Require(fails(sigprocmask_nid_postfix(1, nullptr, readOnly))); + Require(sigprocmask_nid_postfix(1, nullptr, &none) == 0 && std::memcmp(&none, &blocked, sizeof(none)) == 0); + Require(fails(sigprocmask_nid_postfix(1, &set, readOnly))); + Require(sigprocmask_nid_postfix(1, nullptr, &none) == 0 && (none.bits[0] & set.bits[0]) != 0); + Require(sigprocmask_nid_postfix(3, &blocked, nullptr) == 0); + Require(pthread_sigmask_nid_postfix(1, static_cast(unreadable), nullptr) == 14); + Require(pthread_sigmask_nid_postfix(1, nullptr, static_cast(readOnly)) == 14); + Require(pthread_sigmask_nid_postfix(9, static_cast(unreadable), nullptr) == 14); + Require(pthread_sigmask_nid_postfix(9, &set, nullptr) == 22); + Require(sigprocmask_nid_postfix(3, &blocked, nullptr) == 0); + Require(fails(sigaltstack_nid_postfix(static_cast(unreadable), nullptr))); + Require(fails(sigaltstack_nid_postfix(nullptr, static_cast(unreadable)))); + Require(fails(sigaltstack_nid_postfix(nullptr, static_cast(readOnly)))); + Require(fails(sigpending_nid_postfix(static_cast(unreadable)))); + Require(fails(sigpending_nid_postfix(static_cast(readOnly)))); + Require(fails(sigpending_nid_postfix(nullptr))); +} + int main() { const auto invalid = reinterpret_cast(static_cast(-1)); + CheckSignalPointerFaults(); const auto ignore = reinterpret_cast(std::uintptr_t{1}); Require(signal_nid_postfix(9, Callback) == invalid); Require(*__error_nid_postfix() == 22); From 947e459037dc687a5dab6ef850192f5441dd37fe Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Mon, 26 Oct 2026 15:23:10 +0600 Subject: [PATCH 123/702] fix(tools): recognize angle-wrapped documentation links --- tools/check_conventions.py | 6 ++- .../tests/test_check_conventions_doc_links.py | 43 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 8503faf71d..b8869e3fa7 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -67,7 +67,7 @@ CONSTANT = re.compile(rf"\bconstexpr\b[^;=]*?\b([A-Za-z_][A-Za-z0-9_]*)\s*(?:=\s NAMESPACE = re.compile(r"\bnamespace\s+((?:[A-Za-z_][A-Za-z0-9_]*\s*::\s*(?:inline\s+)?)*[A-Za-z_][A-Za-z0-9_]*)\s*$|\bnamespace\s*$") LINKAGE = re.compile(r'\bextern\s*""\s*$') ALIGNMENT_LIMIT = 8192 -LINK = re.compile(r"\[[^\]]+\]\(([^)\s]+)") +LINK = re.compile(r"\[[^\]]+\]\((<[^<>\r\n]*>|(?!<)[^)\s]+)") FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*\r?$|^ {0,3}(?:```|~~~).*\Z", re.S | re.M) INLINE = re.compile(r"`[^`\n]*`") SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) @@ -432,7 +432,9 @@ def links(path, text): result = [] text = INLINE.sub("code", FENCE.sub("", text)) for target in LINK.findall(text): - if re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): + if target.startswith("<"): + target = target[1:-1] + if not target or re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): continue target = target.split("#", 1)[0].split("?", 1)[0] decoded = unquote(target) diff --git a/tools/tests/test_check_conventions_doc_links.py b/tools/tests/test_check_conventions_doc_links.py index ec02294932..a3daebfd91 100644 --- a/tools/tests/test_check_conventions_doc_links.py +++ b/tools/tests/test_check_conventions_doc_links.py @@ -9,6 +9,49 @@ FENCE = "`" * 3 class DocLinkTests(unittest.TestCase): + def test_angle_destinations_resolve_paths_without_the_delimiters(self): + cases = ( + ("../README.md", "README.md"), + ("guide with spaces).md", "docs/guide with spaces).md"), + ("part%23one.md#details", "docs/part#one.md"), + ("guide.md?plain=1#details", "docs/guide.md"), + ("/README.md", "README.md"), + ) + for target, resolved in cases: + for suffix in ("", ' "Guide"', " 'Guide'", " (Guide)"): + with self.subTest(target=target, suffix=suffix): + text = f"[guide](<{target}>{suffix})" + expected = target.split("#", 1)[0].split("?", 1)[0] + self.assertEqual(check_conventions.links("docs/x.md", text), [(expected, resolved)]) + + def test_angle_absolute_urls_fragments_and_empty_destinations_are_ignored(self): + for target in ("https://example.test/guide", "HTTPS://example.test/a b)", "mailto:person@example.test", + "//example.test/guide", "#details", ""): + with self.subTest(target=target): + self.assertEqual(check_conventions.links("docs/x.md", f"[guide](<{target}>)"), []) + + def test_angle_links_inside_code_are_ignored(self): + text = f"`[guide]()`\n{FENCE}\n[guide]()\n{FENCE}\n" + self.assertEqual(check_conventions.links("docs/x.md", text), []) + + def test_bare_destinations_keep_embedded_angle_characters(self): + for target in ("part>one.md", "part Date: Sun, 25 Oct 2026 20:39:57 -0400 Subject: [PATCH 124/702] feat(agc): remap unsupported tessellation viewport depth ranges --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 10 ++ .../Execution/src/Driver/Draw/DrawCapture.cpp | 3 + .../src/Driver/Shaders/ShaderRegistry.cpp | 6 +- .../libSceAgcDriver/Graphics/src/State.cpp | 4 +- .../libSceAgcDriver/tests/DepthRangeRemap.cpp | 61 +++++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 11 ++ .../tests/execution/Tessellation.cpp | 108 +++++++++++++++++- core/shader/recompiler/Recompiler.cpp | 4 +- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 7 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 11 files changed, 191 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 521a3f93bd..59e57ed131 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2405,6 +2405,16 @@ target_include_directories(agc_driver_color565_target_tests PRIVATE $) +if(BUILD_TESTING) + add_test(NAME agc_driver_tessellation_depth_range_remap COMMAND agc_driver_tessellation_tests --depth-remap) + set_tests_properties(agc_driver_tessellation_depth_range_remap PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_tessellation_depth_range_remap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_include_directories(agc_driver_depth_bounds_emulation_draw_tests PRIVATE $) foreach(stage IN ITEMS vert frag) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index a6f6b5ebd9..53a1208810 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -73,6 +73,9 @@ std::shared_ptr Driver::materializeDraw {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : graphics.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}, program.binary.stage == Stage::Mesh ? meshBarycentrics : std::nullopt} }; + if (graphics.depthRemap.mode != 0u && pushOffset < Graphics::PipelinePushSlotBytes) { + request.layout.pushConstantSizeBytes = std::min(request.layout.pushConstantSizeBytes, ShaderRecompiler::DepthRemapPushOffsetBytes - pushOffset); + } const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; auto invocation = [&] { try { 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 312b71958b..d5ead0324f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1785,7 +1785,10 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize; pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots); const auto offset = pushOffset; - const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; + auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; + if (decoded.state.depthRemap.mode != 0u && offset < Graphics::PipelinePushSlotBytes) { + room = std::min(room, ShaderRecompiler::DepthRemapPushOffsetBytes - offset); + } if (deferUnprepared && DeferFailedRegistration(*program.snapshot)) { prepared.push_back({program.snapshot, {program.codeOffset, nullptr}, program.binary.stage, offset, nullptr}); continue; @@ -1801,6 +1804,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod } if (vertex) { vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports; + vertex->depthRemapMode = decoded.state.depthRemap.mode; Graphics::ApplyPointSize(decoded.state, *vertex); } ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory, RegisteredFloatMode(*program.snapshot)}, target, {0, 0, offset, room}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, decoded.state.stages.mesh, decoded.state.stages.tessellation, {}}}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5c920b2440..9782a05418 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1381,7 +1381,9 @@ void ApplyDepthRemap(const Context& context, State& state) { const auto& viewport = state.viewport; const bool inRange = viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1; if (context.depthRangeUnrestricted || inRange) return; - Require(state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "viewport depth outside [0, 1] is remapped on the vertex path only"); + const bool vertex = state.stages.path == ShaderPath::Vertex && !state.stages.tessellation; + const bool tessellation = state.stages.path == ShaderPath::Tessellation && state.stages.tessellation.has_value(); + Require((vertex || tessellation) && !state.stages.mesh, "viewport depth outside [0, 1] is remapped on the vertex and tessellation paths only"); Require(context.depthClamp, "viewport depth outside [0, 1] needs depth clamping to stand in for the console's clamp"); const float clampMin = std::clamp(state.depthClampMin, 0.0f, 1.0f); const float clampMax = std::clamp(state.depthClampMax, 0.0f, 1.0f); diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp index 8452a2ae1a..257d5b4987 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -75,11 +75,13 @@ ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { } constexpr std::array PositionExport{0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array DomainPositionExport{0xf8000941u, 0u, 0xf80008cfu, 0x06050605u, 0xbf810000u}; -ShaderRecompiler::RecompileRequest baseRequest(bool remap) { +ShaderRecompiler::RecompileRequest baseRequest(bool remap, ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex) { using namespace ShaderRecompiler; RecompileRequest request{}; - request.shader = {ShaderStage::Vertex, 0x10000u, PositionExport, 0, {}}; + const auto code = stage == ShaderStage::TessellationEvaluation ? std::span(DomainPositionExport) : std::span(PositionExport); + request.shader = {stage, 0x10000u, code, 0, {}}; request.context.waveSize = 64; ShaderVertexStageInfo vertex{}; vertex.depthRemapMode = 2u; @@ -87,9 +89,13 @@ ShaderRecompiler::RecompileRequest baseRequest(bool remap) { request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; - static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityClipDistance}; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityClipDistance, spv::CapabilityTessellation}; request.target.supportedCapabilities = capabilities; request.target.depthRangeRemap = remap; + if (stage == ShaderStage::TessellationEvaluation) { + request.target.tessellation = TessellationTargetLimits{32u, 128u, 128u, 128u, 4096u, 128u, 128u}; + request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {0u, 3u, 0u, 1u}}; + } request.layout.pushConstantSizeBytes = ShaderRecompiler::DepthRemapPushOffsetBytes; request.useCache = false; return request; @@ -99,9 +105,9 @@ void require(bool condition, const char* what) { if (!condition) throw std::runtime_error(what); } -void preparedTransformTests() { +void preparedTransformTests(ShaderRecompiler::ShaderStage stage) { using namespace ShaderRecompiler; - auto request = baseRequest(true); + auto request = baseRequest(true, stage); request.useCache = true; const auto handle = PrepareShader(request); const auto materialize = [&](std::uint32_t mode) { @@ -120,15 +126,15 @@ void preparedTransformTests() { require(!PreparedShaderInvocation::TryCreate(other, handle).has_value(), "a prepared vertex shader matched a request of another depth remap mode"); } -void pushedTransformTests() { +void pushedTransformTests(ShaderRecompiler::ShaderStage stage) { using namespace ShaderRecompiler; - const auto remapped = Recompile(baseRequest(true)); - const auto plain = Recompile(baseRequest(false)); + const auto remapped = Recompile(baseRequest(true, stage)); + const auto plain = Recompile(baseRequest(false, stage)); const auto facts = inspect(remapped); require(!facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "the depth remap scale or offset is a specialization constant"); require(facts.pushConstantVariable && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u) && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u + 1u), "the remapped vertex shader does not read the scale and offset from the push block"); require(!inspect(plain).pushConstantVariable, "a vertex shader without a depth remap gained a push constant block"); - auto crowded = baseRequest(true); + auto crowded = baseRequest(true, stage); crowded.layout.pushConstantSizeBytes = DepthRemapPushOffsetBytes + 4u; bool refused = false; try { @@ -139,18 +145,41 @@ void pushedTransformTests() { require(refused, "a remapped vertex shader with push data over the depth remap words was accepted"); } +void clipDistanceCapacityTests(ShaderRecompiler::ShaderStage stage) { + using namespace ShaderRecompiler; + static constexpr std::array vertex{0xf80008cfu, 0u, 0xf80008dfu, 0u, 0xf80008efu, 0u, 0xbf810000u}; + static constexpr std::array domain{0xf8000941u, 0u, 0xf80008cfu, 0x05050505u, 0xf80008dfu, 0x05050505u, 0xf80008efu, 0x05050505u, 0xbf810000u}; + auto request = baseRequest(true, stage); + request.shader.code = stage == ShaderStage::TessellationEvaluation ? std::span(domain) : std::span(vertex); + request.context.vertex->paClVsOutCntl = 0xc0003fu; + require(inspect(Recompile(request)).clipDistanceLength == 8u, "the remap did not preserve six guest clip distances beside its two planes"); + for (const auto control : {0xc0007fu, 0xc0403fu, 0xc000ffu}) { + request.context.vertex->paClVsOutCntl = control; + bool refused = false; + try { + static_cast(Recompile(request)); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("two free clip-distance components") != std::string::npos; + } + require(refused, "a remap that exceeds the combined clip and cull distance capacity was accepted"); + } +} + } int main() { using namespace ShaderRecompiler; try { - const auto remapped = inspect(Recompile(baseRequest(true))); - const auto plain = inspect(Recompile(baseRequest(false))); - require(remapped.clipDistanceBuiltIn && remapped.clipDistanceLength == plain.clipDistanceLength + 2u, "the remap did not add its two clip distances"); - require(remapped.multiplies >= plain.multiplies + 2u, "the remap does not scale the clip z and offset it by w"); - require(!plain.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !remapped.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 3u), "unexpected specialization constants"); - preparedTransformTests(); - pushedTransformTests(); + for (const auto stage : {ShaderStage::Vertex, ShaderStage::TessellationEvaluation}) { + const auto remapped = inspect(Recompile(baseRequest(true, stage))); + const auto plain = inspect(Recompile(baseRequest(false, stage))); + require(remapped.clipDistanceBuiltIn && remapped.clipDistanceLength == plain.clipDistanceLength + 2u, "the remap did not add its two clip distances"); + require(remapped.multiplies >= plain.multiplies + 2u, "the remap does not scale the clip z and offset it by w"); + require(!plain.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !remapped.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 3u), "unexpected specialization constants"); + preparedTransformTests(stage); + pushedTransformTests(stage); + clipDistanceCapacityTests(stage); + } std::cout << "Depth range remap tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 335a88d0dd..62efad480c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4672,6 +4672,17 @@ void DepthRemapTests() { floating.depthWrite = true; AgcDriver::Graphics::ApplyDepthRemap(remapping, floating); Require(floating.depthRemap.mode == 1 && floating.viewport.minDepth == 0 && floating.viewport.maxDepth == 1 && floating.depthClamp, "a written float depth target refused clipped clamp bounds"); + auto tessellated = AgcDriver::Graphics::DecodeState(queue); + tessellated.stages.path = AgcDriver::Graphics::ShaderPath::Tessellation; + tessellated.stages.tessellation = ShaderRecompiler::TessellationConfiguration{3u, 3u, 1u, 2u, 2u}; + auto nativeTessellation = tessellated; + AgcDriver::Graphics::ApplyDepthRemap(unrestricted, nativeTessellation); + Require(nativeTessellation.depthRemap.mode == 0 && nativeTessellation.viewport.minDepth == -0.5f && nativeTessellation.viewport.maxDepth == 1.5f, "an unrestricted tessellation draw was remapped"); + AgcDriver::Graphics::ApplyDepthRemap(remapping, tessellated); + Require(tessellated.depthRemap.mode == 1 && tessellated.depthRemap.scale == 2.0f && tessellated.depthRemap.offset == -0.5f && tessellated.depthClamp, "a tessellation draw did not remap its depth range"); + auto combined = nativeTessellation; + combined.stages.mesh = ShaderRecompiler::MeshConfiguration{}; + expectFailure([&] { AgcDriver::Graphics::ApplyDepthRemap(remapping, combined); }, "vertex and tessellation paths only"); } void DepthBoundsEmulationTests() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 7f13530d09..5e8c882ded 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -498,7 +499,7 @@ struct Stages { ShaderRecompiler::RecompileResult fragment; }; -ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, std::uint32_t& pushCursor, const PatchDraw& patches = Triangles) { +ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, std::uint32_t& pushCursor, const PatchDraw& patches = Triangles, std::uint32_t depthRemapMode = 0u) { static const std::array domainUserData{}; const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const bool hull = stage == ShaderStage::TessellationControl; @@ -515,10 +516,12 @@ ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& t pixel = info; } else { vertex = ShaderRecompiler::ShaderVertexStageInfo{}; + vertex->depthRemapMode = depthRemapMode; graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances}}; } auto layout = stage == ShaderStage::Fragment ? PixelPushLayout(pushCursor, target) : ShaderRecompiler::BindingLayout{0, 0, pushCursor, AgcDriver::Graphics::PipelinePushSlotBytes - pushCursor}; if (layout.pushConstantOffsetBytes < AgcDriver::Graphics::PipelinePushSlotBytes) layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes - layout.pushConstantOffsetBytes; + if (depthRemapMode != 0u && layout.pushConstantOffsetBytes < AgcDriver::Graphics::PipelinePushSlotBytes) layout.pushConstantSizeBytes = ShaderRecompiler::DepthRemapPushOffsetBytes - layout.pushConstantOffsetBytes; const ShaderRecompiler::RecompileRequest request{ {stage, reinterpret_cast(code.data()), code, 0, {}}, {64, hull ? 0u : 8u, userData, std::nullopt, pixel, vertex, memory}, @@ -567,7 +570,17 @@ std::uint32_t Row(float y) { return static_cast((1.0f - y) * 0.5f * Height); } -void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags cull, VkFrontFace front, const PatchDraw& patches = Triangles) { +struct DepthDraw { + std::uint64_t address; + bool write; + bool negativeOneToOne = false; + float scale = 1.0f; + float offset = 0.0f; + float clampMin = 0.0f; + float clampMax = 1.0f; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags cull, VkFrontFace front, const PatchDraw& patches = Triangles, const DepthDraw* depth = nullptr) { for (std::size_t i = 0; i < Pixels.size(); i += 4) { for (std::size_t c = 0; c < 4; ++c) Pixels[i + c] = std::byte{Background[c]}; } @@ -598,10 +611,95 @@ void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; + if (depth != nullptr) { + state.depth = AgcDriver::Graphics::DepthTarget{depth->address, 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0u}; + state.depthTest = true; + state.depthWrite = depth->write; + state.depthCompare = depth->write ? VK_COMPARE_OP_ALWAYS : VK_COMPARE_OP_LESS; + state.depthScale = depth->scale; + state.depthOffset = depth->offset; + state.depthClampMin = depth->clampMin; + state.depthClampMax = depth->clampMax; + state.negativeOneToOne = depth->negativeOneToOne; + state.viewport.minDepth = depth->negativeOneToOne ? depth->offset - depth->scale : depth->offset; + state.viewport.maxDepth = depth->offset + depth->scale; + auto context = device.graphicsContext(); + context.depthRangeUnrestricted = false; + AgcDriver::Graphics::ApplyDepthRemap(context, state); + } device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances, 0, true, 0, patches.firstInstance}, shaders); device.WaitIdle(); } +void RequireDepthRangeRemap(AgcDriver::VulkanDevice& device) { + auto target = device.Target(); + Require(device.graphicsContext().depthClamp, "tessellation depth remap needs depth clamping"); + target.depthRangeRemap = true; + const auto compile = [&](float z, std::uint32_t mode) { + std::vector domain(DomainCode.begin(), DomainCode.end()); + const auto literal = std::ranges::find(domain, 0x7e260280u); + Require(literal != domain.end(), "the domain fixture has no constant depth"); + *literal = 0x7e2602ffu; + domain.insert(literal + 1, std::bit_cast(z)); + std::uint32_t cursor = 0u; + return Stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor), + Compile(target, ShaderStage::TessellationControl, HullCode, cursor), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, Triangles, mode), + Compile(target, ShaderStage::Fragment, PixelCode, cursor), + }; + }; + const auto covered = [] { + std::uint32_t count = 0u; + for (std::size_t offset = 0; offset < Pixels.size(); offset += 4u) { + if (Pixels[offset] == std::byte{255}) ++count; + } + return count; + }; + Draw(device, compile(0.5f, 0u), VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE); + const auto expected = covered(); + Require(expected != 0u, "the depth fixture drew no tessellated patches"); + struct Case { + bool symmetric; + float scale; + float offset; + float clampMin; + float clampMax; + float z; + bool drawn; + float probe; + bool passes; + }; + constexpr std::array cases{{ + {true, 1.0f, 0.0f, 0.0f, 1.0f, 0.75f, true, 0.5f, true}, + {true, 1.0f, 0.0f, 0.0f, 1.0f, -0.5f, true, 0.25f, false}, + {true, 1.0f, 0.0f, 0.0f, 1.0f, -1.25f, false, 0.5f, true}, + {true, 1.0f, 0.0f, 0.0f, 1.0f, 1.25f, false, 0.5f, true}, + {false, 2.0f, -0.5f, 0.0f, 1.0f, 0.25f, true, 0.125f, false}, + {false, 2.0f, -0.5f, 0.0f, 1.0f, 0.75f, true, 0.875f, true}, + {false, 2.0f, -0.5f, 0.0f, 1.0f, -0.125f, false, 0.5f, true}, + {false, 2.0f, -0.5f, 0.0f, 1.0f, 1.125f, false, 0.5f, true}, + {false, 2.0f, -0.5f, 0.25f, 0.75f, 0.4f, true, 0.35f, false}, + {false, 2.0f, -0.5f, 0.25f, 0.75f, 0.6f, true, 0.65f, true}, + {false, 2.0f, -0.5f, 0.25f, 0.75f, 0.2f, true, 0.2f, true}, + {false, 2.0f, -0.5f, 0.25f, 0.75f, 0.8f, true, 0.8f, false}, + {false, -2.0f, 1.5f, 0.0f, 1.0f, 0.4f, true, 0.65f, true}, + {false, -2.0f, 1.5f, 0.0f, 1.0f, 0.6f, true, 0.35f, false}, + }}; + alignas(4096) static std::array, cases.size()> depths{}; + for (std::size_t index = 0; index < cases.size(); ++index) { + const auto& test = cases[index]; + const auto address = static_cast(reinterpret_cast(depths[index].data())); + const DepthDraw write{address, true, test.symmetric, test.scale, test.offset, test.clampMin, test.clampMax}; + Draw(device, compile(test.z, test.symmetric ? 2u : 1u), VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, Triangles, &write); + Require(covered() == (test.drawn ? expected : 0u), "tessellation remap clipping differs in case " + std::to_string(index)); + const DepthDraw probe{address, false}; + Draw(device, compile(test.probe, 0u), VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, Triangles, &probe); + Require(covered() == (test.passes ? expected : 0u), "tessellation remap stored depth differs in case " + std::to_string(index)); + } + std::puts("Tessellation depth remap: 14 clipping and stored-depth cases passed"); +} + std::uint32_t CoveredPixels(std::uint32_t patch, bool interior) { std::uint32_t count = 0; for (std::uint32_t y = Row(0.5f); y <= Row(-0.5f) && y < Height; ++y) { @@ -1002,7 +1100,7 @@ void RequireRegisteredMergedStages() { } -int main() { +int main(int argc, char** argv) { try { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -1011,6 +1109,10 @@ int main() { std::puts("tessellation: skipped, the device has no tessellation shaders"); return VulkanTestSkipped; } + if (argc == 2 && std::string_view(argv[1]) == "--depth-remap") { + RequireDepthRangeRemap(*device); + return 0; + } RequireSingleControlPointHulls(target); ExpectRefusal(target, ShaderStage::TessellationControl, HullUnguardedFactors, "not written by control point 0 alone"); ExpectRefusal(target, ShaderStage::TessellationControl, HullSquaredAddress, "not a whole number of control points"); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 5d4ecc297a..9371cde2dc 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1131,10 +1131,10 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (variant.info.info.rasterPrimitive) moduleKey.push_back(static_cast(request.context.pixel->rasterPrimitive)); if (variant.info.info.layerExported) moduleKey.push_back(request.context.pixel->layerExported ? 1u : 0u); } - const bool depthRemap = request.target.depthRangeRemap && request.shader.stage == ShaderStage::Vertex && request.context.vertex.has_value(); + const bool depthRemap = request.target.depthRangeRemap && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::TessellationEvaluation) && request.context.vertex.has_value(); if (depthRemap) { if (request.context.vertex->depthRemapMode != 0u && request.layout.pushConstantOffsetBytes % (RuntimeAbi::PushConstantDwords * 4u) + request.layout.pushConstantSizeBytes > DepthRemapPushOffsetBytes) { - throw std::runtime_error("a remapped vertex shader's push data must end before the depth remap words"); + throw std::runtime_error("a remapped raster shader's push data must end before the depth remap words"); } moduleKey.push_back(request.context.vertex->depthRemapMode); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index f71d15cbca..0fbd448816 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -368,8 +368,11 @@ void DefineOutputs(SpirvEmitterState& state) { DefineMeshOutputs(state); return; } - const bool depthRemap = state.depthRangeRemap && state.program.Resources().stage == IrShaderStage::Vertex; + const bool depthRemap = state.depthRangeRemap && (state.program.Resources().stage == IrShaderStage::Vertex || state.program.Resources().stage == IrShaderStage::TessellationEvaluation); if (depthRemap) { + if (clipDistanceCount + cullDistanceCount > MaxClipDistances - 2u) { + FailEmit("depth remap needs two free clip-distance components"); + } state.depthRemapClipIndex = clipDistanceCount; clipDistanceCount += 2u; } @@ -460,7 +463,7 @@ void DefineOutputs(SpirvEmitterState& state) { void DefineDescriptors(SpirvEmitterState& state) { const IrBindingLayout& layout = state.program.Metadata().bindings; const IrShaderStage stage = state.program.Resources().stage; - if (layout.UsesPushData() || state.stencilReplay || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl || (state.depthRangeRemap && stage == IrShaderStage::Vertex)) { + if (layout.UsesPushData() || state.stencilReplay || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl || (state.depthRangeRemap && (stage == IrShaderStage::Vertex || stage == IrShaderStage::TessellationEvaluation))) { const auto type = PushConstantBlockType(state); state.pushConstantVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant); state.module.AddName(type, "BufferResource"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1bc242f265..6bd00357e0 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -2035,7 +2035,7 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if (state.inputInfo.vertex->clipSpace.enabled) { value = ConvertPositionToClipSpace(state, value); } - if (state.depthRangeRemap && state.program.Resources().stage == IrShaderStage::Vertex) { + if (state.depthRangeRemap && (state.program.Resources().stage == IrShaderStage::Vertex || state.program.Resources().stage == IrShaderStage::TessellationEvaluation)) { value = RemapPositionDepth(state, value); } } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 4976a17071..64d0438157 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -260,7 +260,7 @@ - [Multisample image readback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp) fetches unsigned R8/R16/R32/R32G32/R32G32B32A32 or D32 depth samples through a 2D array view in GENERAL, in layer/y/x/fragment order. The caller owns producer completion, image transitions and resource lifetimes. It implements Vulkan per-sample fetches, not guest-memory publication, metadata expansion or PS5 layout equivalence; D32 exceptional-value preservation has not been measured. Other formats, sample counts and invalid ranges throw. - [Multisample image upload](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp) does not model guest layouts, native ownership or CMASK/FMASK/DCC publication; console equivalence and large-title initialization costs remain unmeasured. It stores 1/2/4/8/16-byte elements from a storage buffer into an unsigned 2D multisample array view in layer/y/x/fragment order. It requires enabled multisample storage images and formatless image writes, 2/4/8 fragments and valid aligned buffer and dispatch ranges. The caller supplies the matching storage view in GENERAL and owns barriers, completion and resource lifetimes. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. -- [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. The mode is a pipeline specialization constant (`PipelineSpecialization::DepthRemapBase`); scale and offset are read from the draw's push block (two words at byte 104, `DepthRemapPushOffsetBytes`, so a remapped draw's stage push data ends before it), so draws that differ only in them share one vertex module and one pipeline. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. +- [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - mesh draws with a viewport depth range outside [0, 1] still throw on devices without `VK_EXT_depth_range_unrestricted`, including combined tessellation and geometry draws. The vertex and tessellation fallback clips `PA_SC_VPORT_ZMIN/ZMAX` to [0, 1], so a negative depth on a float target is stored as 0: Minecraft PPSA17221 requests [-1, 1], which a later test or read at 0 can distinguish. The fallback needs depth clamping and two free components out of the eight combined clip/cull distances. Console clamp/bias ordering and clamp ranges narrower than the viewport transform remain unmeasured. - [Benchmark rewrite failure test off Windows](../../../core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp) (libSceAgcDriver) - `ReplaceGzipFile` checks that a write failing at `.tmp` keeps the previous `aps5-bench.json.gz` only on non-Windows builds: the `std::ios_base::failure` thrown inside the MinGW ofstream did not unwind to the test's catch, so Windows CI covers only the failed move. - [Sampled array windows](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver) - a sampled 2D array, 1D array or cube view past slice 0 is built from its viewed slices only (`ViewedArrayWindow`). The window needs a thin surface without DCC metadata, or with keys that resolve over the whole surface to Uncompressed (the window then drops the metadata; keys are rescanned at each lookup). For a 64 KiB XOR swizzle it starts at the last slice at or before the base whose swizzle slice bits are zero (R_X 1-byte and Z_X 4-byte: a multiple of 16); when that is slice 0 the view builds slices 0 to its last slice. A window and a whole-array view of the same surface are separate images over overlapping memory, kept coherent by the existing overlap rules, at the cost of both. PPSA26344 samples a cube array at slices 4299..7232 at 1467 MiB instead of about 3.8 GB; PPSA01341 samples slice 636 of a DCC 2D array at 52 MiB instead of 2.5 GiB. From 27147acbbd591600b12d98009a86a6aec0aec34f Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Mon, 26 Oct 2026 16:34:17 +0530 Subject: [PATCH 125/702] refactor(macos): keep only the relinker load-command reserve imellag's earlier #11047 (guest TLS) and #11048 (guest start, main-thread SDL) carry the same libkernel imports this PR added in libc, and #10876 carries the same ApplicationProcessParameters change. Drop those parts here and keep only what no open PR has: the 0x400-byte reserve after the Mach-O load commands, so codesign's LC_CODE_SIGNATURE no longer overwrites the entry stub. --- .../libSceVideoOut/include/DisplayWindow.hpp | 6 - .../prx/libSceVideoOut/src/DisplayWindow.cpp | 29 --- .../prx/libSceVideoOut/src/MouseInput.cpp | 24 --- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 17 -- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 68 ------- core/libs/prx/libc/CMakeLists.txt | 14 -- core/libs/prx/libc/include/MainThread.hpp | 37 ---- .../libc/src/ApplicationProcessParameters.cpp | 4 +- .../libc/src/specifics/macos/GuestStart.cpp | 75 -------- .../prx/libc/src/specifics/macos/GuestTls.cpp | 172 ------------------ core/libs/tests/MacOsGuestTlsFirstAccess.cpp | 76 -------- 11 files changed, 2 insertions(+), 520 deletions(-) delete mode 100644 core/libs/prx/libc/include/MainThread.hpp delete mode 100644 core/libs/prx/libc/src/specifics/macos/GuestStart.cpp delete mode 100644 core/libs/prx/libc/src/specifics/macos/GuestTls.cpp delete mode 100644 core/libs/tests/MacOsGuestTlsFirstAccess.cpp diff --git a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp index 1c86d3c7b4..63ba3f0281 100644 --- a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp +++ b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp @@ -22,9 +22,6 @@ public: void UpdateTitle(); std::uint64_t FrameNumber() const { return frameNum; } void ToggleFullscreen(); -#ifdef __APPLE__ - Uint32 Id() const { return windowId; } -#endif private: void create(std::uint32_t sourceWidth, std::uint32_t sourceHeight); @@ -39,9 +36,6 @@ private: std::uint32_t aspectWidth = 0; std::uint32_t aspectHeight = 0; std::uint64_t frameNum = 0; -#ifdef __APPLE__ - Uint32 windowId = 0; -#endif }; #endif diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index b39076dd78..35c6c86917 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -15,10 +15,6 @@ #include "SDL_syswm.h" #include #include -#endif -#ifdef __APPLE__ -#include "prx/libc/include/MainThread.hpp" - #endif namespace { @@ -55,9 +51,6 @@ void DisplayWindow::Ensure(std::uint32_t sourceWidth, std::uint32_t sourceHeight } void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { -#ifdef __APPLE__ - RunOnMainThread([&] { -#endif SDL_Rect usable{}; require(SDL_GetDisplayUsableBounds(0, &usable) == 0, SDL_GetError()); require(DisplayWindowInitialSizePercent > 0 && DisplayWindowInitialSizePercent <= 100, "initial window size percent must be between 1 and 100"); @@ -71,15 +64,9 @@ void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight window = SDL_CreateWindow(title.value, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(initialSize.width), static_cast(initialSize.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); AgcDriverUnlockVulkanLoader_nid_postfix(); require(window != nullptr, SDL_GetError()); -#ifdef __APPLE__ - windowId = SDL_GetWindowID(window); -#endif SDL_SetWindowMinimumSize(window, static_cast(DisplayWindowMinimumWidth), static_cast(DisplayWindowMinimumHeight)); applyAppIcon(window); installSubclass(); -#ifdef __APPLE__ - }); -#endif } void DisplayWindow::updateAspectRatio(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { @@ -90,12 +77,7 @@ void DisplayWindow::updateAspectRatio(std::uint32_t sourceWidth, std::uint32_t s void DisplayWindow::Destroy() noexcept { if (window == nullptr) return; removeSubclass(); -#ifdef __APPLE__ - RunOnMainThread([&] { SDL_DestroyWindow(window); }); - windowId = 0; -#else SDL_DestroyWindow(window); -#endif window = nullptr; } @@ -105,15 +87,8 @@ SDL_Window* DisplayWindow::Handle() const { void DisplayWindow::ToggleFullscreen() { require(window != nullptr, "window must exist before toggling fullscreen"); -#ifdef __APPLE__ - RunOnMainThread([&] { - const auto flags = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0 ? 0u : static_cast(SDL_WINDOW_FULLSCREEN_DESKTOP); - require(SDL_SetWindowFullscreen(window, flags) == 0, SDL_GetError()); - }); -#else const auto flags = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0 ? 0u : static_cast(SDL_WINDOW_FULLSCREEN_DESKTOP); require(SDL_SetWindowFullscreen(window, flags) == 0, SDL_GetError()); -#endif } void DisplayWindow::DrawableSize(std::uint32_t& width, std::uint32_t& height) const { @@ -146,11 +121,7 @@ void DisplayWindow::UpdateTitle() { } char text[160]; std::snprintf(text, sizeof(text), "%s | FPS: %.2f (%llu)", title.value, currentFps, static_cast(frameNum)); -#ifdef __APPLE__ - RunOnMainThread([&] { SDL_SetWindowTitle(window, text); }); -#else SDL_SetWindowTitle(window, text); -#endif if (auto* telemetry = VideoOutTelemetry::Instance(); telemetry != nullptr && (frameNum == 1 || frameNum % 30 == 0)) telemetry->EmitPresent(frameNum, currentFps); } diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index aa084f088b..f1e79af366 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -5,10 +5,6 @@ #include "SDL_video.h" #include #include -#ifdef __APPLE__ -#include -#include "prx/libc/include/MainThread.hpp" -#endif void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { MouseInputEvent input{}; @@ -91,25 +87,6 @@ void MouseInput::UpdateCapture(bool allowed) { return; } const bool wanted = CaptureWanted(allowed); -#ifdef __APPLE__ - bool relative = false; - RunOnMainThread([&] { relative = SDL_GetRelativeMouseMode() == SDL_TRUE; }); - if (wanted == captured && relative == wanted) return; - int result = 0; - std::string error; - RunOnMainThread([&] { - result = SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE); - if (result != 0) error = SDL_GetError(); - }); - if (result != 0) { - static bool warned = false; - if (!warned) { - warned = true; - std::fprintf(stderr, "[mouse] relative mouse mode unavailable: %s\n", error.c_str()); - } - return; - } -#else if (wanted == captured && (SDL_GetRelativeMouseMode() == SDL_TRUE) == wanted) return; if (SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE) != 0) { static bool warned = false; @@ -119,7 +96,6 @@ void MouseInput::UpdateCapture(bool allowed) { } return; } -#endif int ignoredX = 0; int ignoredY = 0; SDL_GetRelativeMouseState(&ignoredX, &ignoredY); diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 516a806728..5e9ac3fcf7 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -12,9 +12,6 @@ #include "prx/libSceMouse/include/MouseState.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" -#ifdef __APPLE__ -#include "prx/libc/include/MainThread.hpp" -#endif #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/Pad.hpp" @@ -171,15 +168,7 @@ void PadInput::applyOutput() { void PadInput::setMouseMode(bool enabled) { SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0"); SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0"); -#ifdef __APPLE__ - std::string error; - RunOnMainThread([&] { - if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) error = SDL_GetError(); - }); - if (!error.empty()) throw std::runtime_error("Pad: relative mouse mode failed: " + error); -#else if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError()); -#endif keyboardMouse.SetMouseMode(enabled); } @@ -302,15 +291,9 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (event.type == SDL_KEYDOWN && ImeCapturesKeyboard_nid_postfix()) return; keyboardMouse.SetTitleReadsMouse(MouseIsOpen_nid_no_patch()); const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()); -#ifdef __APPLE__ - if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == window.Id()) window.ToggleFullscreen(); - if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == window.Id()) AgcDriverTogglePerformanceOverlay_nid_postfix(); - if (requests.screenshot && window.Handle() != nullptr && event.key.windowID == window.Id()) AgcDriverRequestScreenshot_nid_postfix(); -#else if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix(); if (requests.screenshot && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverRequestScreenshot_nid_postfix(); -#endif if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); if (event.type != SDL_MOUSEMOTION) publish(window.FrameNumber(), 0); } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 1992f2513d..377fb3548b 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -33,9 +33,6 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/Shutdown.hpp" -#ifdef __APPLE__ -#include "prx/libc/include/MainThread.hpp" -#endif namespace { @@ -67,53 +64,21 @@ std::optional configuredResolution() { } std::vector windowExtensions(SDL_Window* window) { -#ifdef __APPLE__ - std::vector extensions; - RunOnMainThread([&] { - unsigned extensionCount = 0; - if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); - extensions.resize(extensionCount); - if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); - extensions.resize(extensionCount); - }); - return extensions; -#else unsigned extensionCount = 0; if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); std::vector extensions(extensionCount); if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); extensions.resize(extensionCount); return extensions; -#endif } VkSurfaceKHR createWindowSurface(void* context, VkInstance instance) { VkSurfaceKHR surface = VK_NULL_HANDLE; -#ifdef __APPLE__ - RunOnMainThread([&] { - if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError()); - }); -#else if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError()); -#endif return surface; } void windowDrawableSize(void* context, std::uint32_t* width, std::uint32_t* height) { -#ifdef __APPLE__ - bool minimized = false; - int drawableWidth = 0; - int drawableHeight = 0; - RunOnMainThread([&] { - minimized = (SDL_GetWindowFlags(static_cast(context)) & SDL_WINDOW_MINIMIZED) != 0; - if (!minimized) SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); - }); - if (minimized) { - *width = 0; - *height = 0; - return; - } -#else if ((SDL_GetWindowFlags(static_cast(context)) & SDL_WINDOW_MINIMIZED) != 0) { *width = 0; *height = 0; @@ -122,7 +87,6 @@ void windowDrawableSize(void* context, std::uint32_t* width, std::uint32_t* heig int drawableWidth = 0; int drawableHeight = 0; SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); -#endif *width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; *height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; } @@ -335,17 +299,9 @@ VideoOutDriver::VideoOutDriver() { #ifdef __linux__ SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); #endif -#ifdef __APPLE__ - std::string initError; - RunOnMainThread([&] { - if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) initError = SDL_GetError(); - }); - if (!initError.empty()) throw std::runtime_error("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: " + initError); -#else if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); } -#endif try { AgcDriverWaitIdle_nid_postfix(); std::promise started; @@ -561,19 +517,10 @@ std::uint32_t VideoOutDriver::GetDisplayResolution() { } void VideoOutDriver::updateDisplayRefreshRate() { -#ifdef __APPLE__ - SDL_DisplayMode mode{}; - RunOnMainThread([&] { - const int display = SDL_GetWindowDisplayIndex(window.Handle()); - if (display < 0) throw std::runtime_error(std::string("SDL_GetWindowDisplayIndex failed: ") + SDL_GetError()); - if (SDL_GetCurrentDisplayMode(display, &mode) != 0) throw std::runtime_error(std::string("SDL_GetCurrentDisplayMode failed: ") + SDL_GetError()); - }); -#else const int display = SDL_GetWindowDisplayIndex(window.Handle()); if (display < 0) throw std::runtime_error(std::string("SDL_GetWindowDisplayIndex failed: ") + SDL_GetError()); SDL_DisplayMode mode{}; if (SDL_GetCurrentDisplayMode(display, &mode) != 0) throw std::runtime_error(std::string("SDL_GetCurrentDisplayMode failed: ") + SDL_GetError()); -#endif displayRefreshRate.store(mode.refresh_rate); displayResolution.store(resolutionIndex(mode.w, mode.h)); } @@ -749,27 +696,16 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } SDL_Event event; -#ifdef __APPLE__ - RunOnMainThread([] { SDL_PumpEvents(); }); - while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) > 0) { -#else while (SDL_PollEvent(&event)) { -#endif if (event.type == SDL_QUIT) { LibcRequestExit_nid_postfix(0); throw ProcessShutdown{}; } padInput.HandleEvent(event, window); if (window.Handle() != nullptr) { -#ifdef __APPLE__ - mouseInput.HandleEvent(event, window.Id()); - keyboardInput.HandleEvent(event, window.Id()); - imeTextInput.HandleEvent(event, window.Id()); -#else mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); keyboardInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); imeTextInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); -#endif } } padInput.Update(window); @@ -783,11 +719,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star processFlip(*current); int windowWidth = 0; int windowHeight = 0; -#ifdef __APPLE__ - if (window.Handle() != nullptr) RunOnMainThread([&] { SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); }); -#else if (window.Handle() != nullptr) SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); -#endif if (windowWidth > 0 && windowHeight > 0) { const auto presented = AgcDriver::ComputeContainSize_nid_postfix(current->width, current->height, static_cast(windowWidth), static_cast(windowHeight), true); mouseInput.SetPresentation(current->width, current->height, presented.width, presented.height); diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d7a02a8108..7930a2f5fc 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -46,8 +46,6 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/x86_64/CallTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestImages.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestTls.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestStart.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AllocatingStrings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/WideConversion.cpp @@ -173,18 +171,6 @@ if(WIN32) target_link_libraries(libc PRIVATE advapi32 ws2_32 iphlpapi) endif() -if(APPLE) - target_link_libraries(libc PRIVATE "-framework CoreFoundation") - if(BUILD_TESTING) - add_test_executable(macos_guest_tls_first_access_tests - ${LIBS_INCLUDE_DIR}/tests/MacOsGuestTlsFirstAccess.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestTls.cpp) - target_include_directories(macos_guest_tls_first_access_tests PRIVATE ${LIBS_INCLUDE_DIR}) - add_test(NAME macos_guest_tls_first_access COMMAND macos_guest_tls_first_access_tests) - set_tests_properties(macos_guest_tls_first_access PROPERTIES TIMEOUT 60) - endif() -endif() - if(BUILD_TESTING) add_test_executable(guest_fopen_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestFopenErrors.cpp) target_include_directories(guest_fopen_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libc/include/MainThread.hpp b/core/libs/prx/libc/include/MainThread.hpp deleted file mode 100644 index e68b3d52fd..0000000000 --- a/core/libs/prx/libc/include/MainThread.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_MAINTHREAD_HPP -#define CORE_LIBS_PRX_LIBC_INCLUDE_MAINTHREAD_HPP - -#ifdef __APPLE__ - -#include -#include -#include -#include - -extern "C" bool LibcMainQueueServed_nid_postfix(); - -template -void RunOnMainThread(Work&& work) { - if (pthread_main_np() != 0 || !LibcMainQueueServed_nid_postfix()) { - work(); - return; - } - using Callable = std::remove_reference_t; - struct Context { - Callable* Call; - std::exception_ptr Failure; - } context{&work, nullptr}; - dispatch_sync_f(dispatch_get_main_queue(), &context, [](void* argument) { - auto* call = static_cast(argument); - try { - (*call->Call)(); - } catch (...) { - call->Failure = std::current_exception(); - } - }); - if (context.Failure) std::rethrow_exception(context.Failure); -} - -#endif - -#endif diff --git a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp index 8a556bd0e0..5efa1f00d3 100644 --- a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp +++ b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp @@ -10,7 +10,7 @@ #define NOMINMAX #endif #include -#elif defined(__linux__) || defined(__APPLE__) +#elif defined(__linux__) #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include #endif @@ -77,7 +77,7 @@ const void* ApplicationProcessParameters_nid_no_patch() { } if (result == nullptr) throw std::runtime_error("application heap: process metadata is missing; relink the executable"); return result; -#elif defined(__linux__) || defined(__APPLE__) +#elif defined(__linux__) struct Search { const void* parameters = nullptr; bool invalid = false; diff --git a/core/libs/prx/libc/src/specifics/macos/GuestStart.cpp b/core/libs/prx/libc/src/specifics/macos/GuestStart.cpp deleted file mode 100644 index c5eaa85ae1..0000000000 --- a/core/libs/prx/libc/src/specifics/macos/GuestStart.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#ifdef __APPLE__ - - -#include "prx/libc/include/general/AsmMacros.hpp" -#include -#include -#include -#include -#include -#include -#include - -extern "C" [[noreturn]] void Aps5EnterGuest_nid_no_patch(void* entry, void* block); - -asm(".text\n" - ".p2align 4\n" APS5_ASM_FUNCTION("Aps5EnterGuest_nid_no_patch") " movq %rdi, %rax\n" - " movq %rsi, %rdi\n" - " andq $-16, %rsp\n" - " pushq $0\n" - " pushq $0\n" - " movq %rsp, %rbp\n" - " xorl %esi, %esi\n" - " call *%rax\n" - " movl %eax, %edi\n" - " call " APS5_ASM_SYMBOL("exit") "\n" - " ud2\n" - APS5_ASM_FUNCTION_END("Aps5EnterGuest_nid_no_patch")); - -namespace { - -constexpr std::size_t GuestMainStackSize = std::size_t{64} << 20; - -std::atomic mainQueueServed{false}; - -struct GuestStart { - void* Entry; - void* Block; -}; - -void* RunGuestMain(void* argument) { - const auto start = *static_cast(argument); - pthread_setname_np("main"); - Aps5EnterGuest_nid_no_patch(start.Entry, start.Block); -} - -[[noreturn]] void Fatal(const char* message, int error) { - std::fprintf(stderr, "guest start: %s (%d)\n", message, error); - std::abort(); -} - -} - -extern "C" bool LibcMainQueueServed_nid_postfix() { - return mainQueueServed.load(std::memory_order_acquire); -} - -extern "C" [[noreturn]] void Aps5StartGuest_nid_no_patch(void* entry, void* block) { - static GuestStart start; - start = {entry, block}; - pthread_attr_t attributes; - int result = pthread_attr_init(&attributes); - if (result != 0) Fatal("pthread_attr_init failed", result); - result = pthread_attr_setstacksize(&attributes, GuestMainStackSize); - if (result != 0) Fatal("pthread_attr_setstacksize failed", result); - result = pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED); - if (result != 0) Fatal("pthread_attr_setdetachstate failed", result); - mainQueueServed.store(true, std::memory_order_release); - pthread_t thread; - result = pthread_create(&thread, &attributes, RunGuestMain, &start); - if (result != 0) Fatal("pthread_create failed", result); - pthread_attr_destroy(&attributes); - for (;;) CFRunLoopRun(); -} - -#endif diff --git a/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp b/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp deleted file mode 100644 index e29f4171f1..0000000000 --- a/core/libs/prx/libc/src/specifics/macos/GuestTls.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#ifdef __APPLE__ - - -#include "prx/libc/include/general/AsmMacros.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - -struct GuestTlsDescriptor { - std::uint64_t Active; - std::uint64_t InitImage; - std::uint64_t FileSize; - std::uint64_t BlockSize; - std::uint64_t Alignment; - std::uint64_t Key; -}; -static_assert(sizeof(GuestTlsDescriptor) == 0x30); - -constexpr std::size_t ThreadControlBlockSize = 0x30; -constexpr std::size_t AllocationSlot = 0x08; -constexpr std::size_t StackGuardSlot = 0x28; - -[[noreturn]] void Fatal(const char* message) { - std::fprintf(stderr, "guest TLS: %s\n", message); - std::abort(); -} - -std::mutex& RegistrationLock() { - static std::mutex lock; - return lock; -} - -void FreeBlock(void* threadPointer) { - if (threadPointer == nullptr) return; - void* block = nullptr; - std::memcpy(&block, static_cast(threadPointer) + AllocationSlot, sizeof(block)); - std::free(block); -} - -std::atomic_ref KeyOf(GuestTlsDescriptor* descriptor) { - static_assert(std::atomic_ref::is_always_lock_free); - return std::atomic_ref(descriptor->Key); -} - -pthread_key_t RegisteredKey(GuestTlsDescriptor* descriptor) { - auto key = KeyOf(descriptor); - if (const auto value = key.load(std::memory_order_acquire); value != 0) return static_cast(value); - std::lock_guard lock(RegistrationLock()); - if (const auto value = key.load(std::memory_order_relaxed); value != 0) return static_cast(value); - pthread_key_t created{}; - if (pthread_key_create(&created, FreeBlock) != 0) Fatal("pthread_key_create failed"); - if (created == 0) Fatal("pthread_key_create returned key 0"); - key.store(created, std::memory_order_release); - return created; -} - -std::uint64_t StackGuardValue() { - static const std::uint64_t value = [] { - std::uint64_t guard = 0; - arc4random_buf(&guard, sizeof(guard)); - return guard & ~std::uint64_t{0xff}; - }(); - return value; -} - -} - -extern "C" std::uint8_t Aps5GuestTlsUseXsave = [] { - unsigned eax = 0, ebx = 0, ecx = 0, edx = 0; - if (!__get_cpuid(1, &eax, &ebx, &ecx, &edx)) return std::uint8_t{0}; - return static_cast((ecx & bit_OSXSAVE) != 0); -}(); - -extern "C" void Aps5GuestTlsRegister_nid_no_patch(GuestTlsDescriptor* descriptor) { - RegisteredKey(descriptor); -} - -extern "C" void* Aps5GuestTlsAllocateBlock_nid_no_patch(GuestTlsDescriptor* descriptor) { - const auto key = RegisteredKey(descriptor); - if (void* existing = pthread_getspecific(key)) return existing; - const std::size_t alignment = descriptor->Alignment > 16 ? descriptor->Alignment : 16; - const std::size_t tlsSize = (descriptor->BlockSize + alignment - 1) & ~(alignment - 1); - const std::size_t total = tlsSize + ThreadControlBlockSize; - void* block = nullptr; - if (posix_memalign(&block, alignment, total) != 0 || block == nullptr) Fatal("out of memory for a thread block"); - auto* bytes = static_cast(block); - std::memset(bytes, 0, total); - auto* threadPointer = bytes + tlsSize; - auto* image = threadPointer - descriptor->BlockSize; - if (descriptor->FileSize != 0 && descriptor->InitImage != 0) - std::memcpy(image, reinterpret_cast(descriptor->InitImage), descriptor->FileSize); - void* self = threadPointer; - std::memcpy(threadPointer, &self, sizeof(self)); - std::memcpy(threadPointer + AllocationSlot, &block, sizeof(block)); - const auto guard = StackGuardValue(); - std::memcpy(threadPointer + StackGuardSlot, &guard, sizeof(guard)); - if (pthread_setspecific(key, threadPointer) != 0) Fatal("pthread_setspecific failed"); - return threadPointer; -} - -struct GuestTlsIndex { - std::uint64_t Module; - std::uint64_t Offset; -}; - -extern "C" void* Aps5GuestTlsGetAddr_nid_no_patch(const GuestTlsIndex* index) { - auto* descriptor = reinterpret_cast(index->Module); - if (descriptor == nullptr) Fatal("__tls_get_addr with no module"); - auto* threadPointer = static_cast(pthread_getspecific(RegisteredKey(descriptor))); - if (threadPointer == nullptr) threadPointer = static_cast(Aps5GuestTlsAllocateBlock_nid_no_patch(descriptor)); - return threadPointer - descriptor->BlockSize + index->Offset; -} - -asm( - ".text\n" - ".p2align 4\n" - APS5_ASM_FUNCTION("Aps5GuestTlsAllocate_nid_no_patch") - " pushq %rbp\n" - " movq %rsp, %rbp\n" - " pushq %rcx\n" - " pushq %rdx\n" - " pushq %rsi\n" - " pushq %rdi\n" - " pushq %r8\n" - " pushq %r9\n" - " pushq %r10\n" - " pushq %r11\n" - " movq %rax, %rdi\n" - " andq $-64, %rsp\n" - " subq $4096, %rsp\n" - " cmpb $0, " APS5_ASM_SYMBOL("Aps5GuestTlsUseXsave") "(%rip)\n" - " je 1f\n" - " movq %rsp, %rcx\n" - " movl $512, %edx\n" - "2: movq $0, (%rcx,%rdx)\n" - " addq $8, %rdx\n" - " cmpq $576, %rdx\n" - " jb 2b\n" - " movl $-1, %eax\n" - " movl $-1, %edx\n" - " xsave64 (%rsp)\n" - " call " APS5_ASM_SYMBOL("Aps5GuestTlsAllocateBlock_nid_no_patch") "\n" - " movq %rax, %rsi\n" - " movl $-1, %eax\n" - " movl $-1, %edx\n" - " xrstor64 (%rsp)\n" - " movq %rsi, %rax\n" - " jmp 3f\n" - "1: fxsave64 (%rsp)\n" - " call " APS5_ASM_SYMBOL("Aps5GuestTlsAllocateBlock_nid_no_patch") "\n" - " fxrstor64 (%rsp)\n" - "3: leaq -64(%rbp), %rsp\n" - " popq %r11\n" - " popq %r10\n" - " popq %r9\n" - " popq %r8\n" - " popq %rdi\n" - " popq %rsi\n" - " popq %rdx\n" - " popq %rcx\n" - " popq %rbp\n" - " ret\n" - APS5_ASM_FUNCTION_END("Aps5GuestTlsAllocate_nid_no_patch")); - -#endif diff --git a/core/libs/tests/MacOsGuestTlsFirstAccess.cpp b/core/libs/tests/MacOsGuestTlsFirstAccess.cpp deleted file mode 100644 index 6d2b6c0f5f..0000000000 --- a/core/libs/tests/MacOsGuestTlsFirstAccess.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -struct GuestTlsDescriptor { - std::uint64_t Active; - std::uint64_t InitImage; - std::uint64_t FileSize; - std::uint64_t BlockSize; - std::uint64_t Alignment; - std::uint64_t Key; -}; -static_assert(sizeof(GuestTlsDescriptor) == 0x30); - -struct GuestTlsIndex { - std::uint64_t Descriptor; - std::uint64_t Offset; -}; - -extern "C" void* Aps5GuestTlsGetAddr_nid_no_patch(const GuestTlsIndex* index); -extern "C" void* Aps5GuestTlsAllocateBlock_nid_no_patch(GuestTlsDescriptor* descriptor); - -namespace { - -void Require(bool condition, const char* message, int round) { - if (condition) return; - std::fprintf(stderr, "round %d: %s\n", round, message); - std::exit(1); -} - -void RaceFirstAccess(int round, bool throughIndex) { - constexpr int Threads = 16; - alignas(64) static GuestTlsDescriptor descriptor{}; - descriptor = GuestTlsDescriptor{1, 0, 0, 64, 16, 0}; - std::atomic ready{0}; - std::atomic recorded{0}; - std::atomic start{false}; - std::vector blocks(Threads, nullptr); - std::vector again(Threads, nullptr); - std::vector keys(Threads, 0); - std::vector pool; - for (int thread = 0; thread < Threads; ++thread) - pool.emplace_back([&, thread] { - ready.fetch_add(1); - while (!start.load(std::memory_order_acquire)) {} - GuestTlsIndex index{reinterpret_cast(&descriptor), 8}; - blocks[thread] = throughIndex ? Aps5GuestTlsGetAddr_nid_no_patch(&index) - : Aps5GuestTlsAllocateBlock_nid_no_patch(&descriptor); - keys[thread] = std::atomic_ref(descriptor.Key).load(std::memory_order_acquire); - again[thread] = throughIndex ? Aps5GuestTlsGetAddr_nid_no_patch(&index) - : Aps5GuestTlsAllocateBlock_nid_no_patch(&descriptor); - recorded.fetch_add(1); - while (recorded.load() != Threads) {} - }); - while (ready.load() != Threads) {} - start.store(true, std::memory_order_release); - for (auto& thread : pool) thread.join(); - const std::set distinctKeys(keys.begin(), keys.end()); - const std::set distinctBlocks(blocks.begin(), blocks.end()); - Require(distinctKeys.size() == 1 && *distinctKeys.begin() != 0, "racing first accesses registered more than one key", round); - Require(distinctBlocks.size() == Threads && distinctBlocks.count(nullptr) == 0, "threads did not get distinct blocks", round); - for (int thread = 0; thread < Threads; ++thread) - Require(again[thread] == blocks[thread], "a thread's second access returned a different block", round); -} - -} - -int main() { - for (int round = 0; round < 100; ++round) RaceFirstAccess(round, true); - for (int round = 0; round < 100; ++round) RaceFirstAccess(round, false); - return 0; -} From 0424f48dd378b67a27644b8c9a9acf65a59a5bf1 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 26 Oct 2026 00:59:18 -0400 Subject: [PATCH 126/702] fix(tools): compare timed traces by requested window --- tools/performance_compare.py | 5 +++- tools/tests/test_performance_compare.py | 38 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/tools/performance_compare.py b/tools/performance_compare.py index 6a6c599351..9bee4d486f 100644 --- a/tools/performance_compare.py +++ b/tools/performance_compare.py @@ -1,6 +1,7 @@ import argparse import json import math +import re import sys from pathlib import Path @@ -113,7 +114,9 @@ def compare(baseline, candidate): for summary in (baseline, candidate): context = {key: summary[key] for key in CONTEXT} context.update({key: summary[key] for key in (*OPTIONAL_STRINGS, *OPTIONAL_INTEGERS) if key in summary}) - context.update({f"window.{key}": summary["window"][key] for key in ("spec", "first_present")}) + context["window.spec"] = summary["window"]["spec"] + if re.fullmatch(r"seconds [0-9]+:[0-9]+", summary["window"]["spec"]) is None: + context["window.first_present"] = summary["window"]["first_present"] context.update({f"env.{key}": value for key, value in summary["env"].items()}) contexts.append(context) differences = {key: {"baseline": contexts[0].get(key), "candidate": contexts[1].get(key)} diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py index 4016f14439..793835ea3a 100644 --- a/tools/tests/test_performance_compare.py +++ b/tools/tests/test_performance_compare.py @@ -316,6 +316,44 @@ class PerformanceCompareTests(unittest.TestCase): self.write(self.after, candidate) self.assertEqual(self.run_cli("--require-matching-context").returncode, 2) + def test_timed_windows_allow_different_first_present_counters(self): + for summary in (self.baseline, self.candidate): + summary["env"] = {"APS5_TRACE_SECONDS": "270:20"} + summary["window"].update(spec="seconds 270:20", ms=20000.0) + self.baseline["window"]["first_present"] = 1010 + self.candidate["window"]["first_present"] = 700 + self.baseline["presents"] = 20 + self.candidate["presents"] = 40 + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli("--require-matching-context", "--json") + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["configuration_differences"], {}) + self.assertEqual(report["baseline"], self.baseline) + self.assertEqual(report["candidate"], self.candidate) + self.assertEqual(report["metrics_ms"]["mean_frame_ms"]["change_pct"], -50.0) + + def test_timed_windows_still_require_matching_requested_windows(self): + for summary in (self.baseline, self.candidate): + summary["env"] = {"APS5_TRACE_SECONDS": "270:20"} + summary["window"]["spec"] = "seconds 270:20" + self.candidate["window"].update(spec="seconds 280:20", first_present=700) + report = performance_compare.compare(self.baseline, self.candidate) + self.assertEqual(set(report["configuration_differences"]), {"window.spec"}) + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli("--require-matching-context") + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("window.spec", result.stderr) + + def test_unknown_window_syntax_keeps_first_present_check(self): + for summary in (self.baseline, self.candidate): + summary["window"]["spec"] = "seconds unknown" + self.candidate["window"]["first_present"] = 700 + report = performance_compare.compare(self.baseline, self.candidate) + self.assertEqual(set(report["configuration_differences"]), {"window.first_present"}) + def test_main_reconfigures_streams(self): From 02011bb1081b50664733110004146d7158b6bcf7 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 27 Oct 2026 02:05:52 +1300 Subject: [PATCH 127/702] test(agc): age the idle import by frames in the address copy cap tests RunAddressCopyCapTests expects the idle range to be evictable after the busy range's lookups, which an import used in the current frame no longer is, so the build copied 2 MiB past the 1 MiB cap and aborted. The test now advances two frames before the lookups; its assertions are unchanged. --- core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index d6452dcefd..1ce3cce190 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -3639,6 +3639,8 @@ void RunAddressCopyCapTests(const Context& context, const BdaTestAccess& access, SetAddressCopyLimit(0); unrelated(true); } + NoteHostImportFrame(); + NoteHostImportFrame(); for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, busy, 64) != nullptr, "the busy range lost its import"); if (!remembered) unwatch(); const auto refused = builds(); From ddab03671fa101033d06299516a53ee7c1486701 Mon Sep 17 00:00:00 2001 From: Tehshortbus Date: Mon, 26 Oct 2026 05:29:51 -0700 Subject: [PATCH 128/702] chore(agc): log texture creation only with APS5_DEBUG_LOG, except the sRGB copy GTA V (PPSA04264) creates textures throughout gameplay; the unconditional creation lines and their per-mip lines were 68,694 of 69,889 lines (98 %) of a 7-minute run's log. 81c67e1be put the storage image and storage-viewed texture lines behind APS5_DEBUG_LOG; this does the same for the guest-memory, repacked, block and depth copy paths. The sRGB copy line stays unconditional: #7085 changes the lines on both sides of it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ada06eab1e..b0a9c0d0b5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -353,9 +353,11 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto& mips = geometry.mips; const auto arrayLayers = geometry.layers; const auto elementBytes = BytesPerElement(descriptor.format); +#ifdef APS5_DEBUG_LOG AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u dim=%d tile=%d format=%u vk=%d element=%u", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, arrayLayers, static_cast(descriptor.dimension), static_cast(descriptor.tileMode), descriptor.format, static_cast(vkFormat), elementBytes); for (const auto& mip : mips) AGC_LOG_DEFERRED(stdout, " mip %ux%u tiled=0x%llx+0x%llx linear=0x%llx+0x%llx blocksPerRow=%u pitch=%u tail=%d", mip.width, mip.height, static_cast(mip.tiledOffset), static_cast(mip.tiledSize), static_cast(mip.linearOffset), static_cast(mip.linearSize), mip.blocksPerRow, mip.pitchBytes, mip.tail ? 1 : 0); +#endif const auto guestBytes = geometry.guestBytes; Require(snapshot.size() == guestBytes, "texture snapshot size mismatch"); @@ -641,7 +643,9 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyLevels = descriptor.mipCount; copyLayers = geometry.imageLayers; copyElementBytes = BytesPerElement(descriptor.format); +#ifdef APS5_DEBUG_LOG AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u repacked copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, descriptor.format, static_cast(format)); +#endif VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; @@ -700,7 +704,9 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyLevels = descriptor.mipCount; copyLayers = geometry.imageLayers; copyElementBytes = BytesPerElement(descriptor.format); +#ifdef APS5_DEBUG_LOG AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast(blockFormat)); +#endif VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.imageType = VK_IMAGE_TYPE_2D; @@ -758,7 +764,9 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyLevels = descriptor.mipCount; copyLayers = geometry.imageLayers; copyElementBytes = BytesPerElement(descriptor.format); +#ifdef APS5_DEBUG_LOG AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast(colorFormat), static_cast(depthFormat)); +#endif const bool depth1D = descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray; VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; From a0b14febf95401416c8ccc93b60919b470940e5a Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:50:54 -0400 Subject: [PATCH 129/702] fix(libSceLibreSsl): declare the ex_data callbacks as LibreSSL's function pointer types SSL_get_ex_new_index and SSL_CTX_get_ex_new_index took the new, dup and free callbacks as void*, which a caller passing real callbacks can't convert to. They now take LibreSslExNew, LibreSslExDup and LibreSslExFree, the guest-ABI forms of CRYPTO_EX_new, CRYPTO_EX_dup and CRYPTO_EX_free from LibreSSL 2.5.4's ossl_typ.h. Reported by @MarcZX10. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/include/Api.hpp | 12 ++++++++++-- core/libs/prx/libSceLibreSsl/src/ExData.cpp | 10 +++++----- core/libs/prx/libSceLibreSsl/tests/ExData.cpp | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index e9bb873e42..d23e41cd07 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -9,6 +9,14 @@ struct Asn1String { std::int64_t flags; }; +struct LibreSslExData { + void* stack; +}; + +using LibreSslExNew = int (APS5_VABI*)(void* parent, void* pointer, LibreSslExData* data, int index, std::int64_t argl, void* argp); +using LibreSslExFree = void (APS5_VABI*)(void* parent, void* pointer, LibreSslExData* data, int index, std::int64_t argl, void* argp); +using LibreSslExDup = int (APS5_VABI*)(LibreSslExData* to, LibreSslExData* from, void* fromData, int index, std::int64_t argl, void* argp); + inline constexpr int MaxRounds = 14; struct AesKey { @@ -263,7 +271,7 @@ int APS5_VABI SSL_CTX_ctrl_nid_postfix(void); int APS5_VABI SSL_CTX_free_nid_postfix(void); int APS5_VABI SSL_CTX_get_cert_store_nid_postfix(void); int APS5_VABI SSL_CTX_get_ex_data_nid_postfix(void); -int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, void* newFunction, void* dupFunction, void* freeFunction); +int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, LibreSslExNew newFunction, LibreSslExDup dupFunction, LibreSslExFree freeFunction); int APS5_VABI SSL_CTX_get_verify_mode_nid_postfix(void); int APS5_VABI SSL_CTX_load_verify_locations_nid_postfix(void); int APS5_VABI SSL_CTX_load_verify_mem_nid_postfix(void); @@ -308,7 +316,7 @@ int APS5_VABI SSL_get_current_cipher_nid_postfix(void); int APS5_VABI SSL_get_error_nid_postfix(void); int APS5_VABI SSL_get_ex_data_nid_postfix(void); int APS5_VABI SSL_get_ex_data_X509_STORE_CTX_idx_nid_postfix(void); -int APS5_VABI SSL_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, void* newFunction, void* dupFunction, void* freeFunction); +int APS5_VABI SSL_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, LibreSslExNew newFunction, LibreSslExDup dupFunction, LibreSslExFree freeFunction); int APS5_VABI SSL_get_fd_nid_postfix(void); int APS5_VABI SSL_get_peer_cert_chain_nid_postfix(void); int APS5_VABI SSL_get_peer_certificate_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/ExData.cpp b/core/libs/prx/libSceLibreSsl/src/ExData.cpp index 24a361df60..da6d14cf51 100644 --- a/core/libs/prx/libSceLibreSsl/src/ExData.cpp +++ b/core/libs/prx/libSceLibreSsl/src/ExData.cpp @@ -18,9 +18,9 @@ constexpr int ClassCount = 16; struct ExDataFunctions { std::int64_t argl; void* argp; - void* newFunction; - void* dupFunction; - void* freeFunction; + LibreSslExNew newFunction; + LibreSslExDup dupFunction; + LibreSslExFree freeFunction; }; std::mutex classesLock; @@ -43,11 +43,11 @@ int NewIndex(int classIndex, const ExDataFunctions& functions) { extern "C" { -int APS5_VABI SSL_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, void* newFunction, void* dupFunction, void* freeFunction) { +int APS5_VABI SSL_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, LibreSslExNew newFunction, LibreSslExDup dupFunction, LibreSslExFree freeFunction) { return NewIndex(SslClass, {argl, argp, newFunction, dupFunction, freeFunction}); } -int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, void* newFunction, void* dupFunction, void* freeFunction) { +int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(std::int64_t argl, void* argp, LibreSslExNew newFunction, LibreSslExDup dupFunction, LibreSslExFree freeFunction) { return NewIndex(SslContextClass, {argl, argp, newFunction, dupFunction, freeFunction}); } diff --git a/core/libs/prx/libSceLibreSsl/tests/ExData.cpp b/core/libs/prx/libSceLibreSsl/tests/ExData.cpp index fec806bfed..b037aaaf1f 100644 --- a/core/libs/prx/libSceLibreSsl/tests/ExData.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/ExData.cpp @@ -1,4 +1,5 @@ #include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include #include #include #include @@ -12,6 +13,18 @@ void Require(bool condition, const char* message) { std::abort(); } +int APS5_VABI NewCallback(void*, void*, LibreSslExData*, int, std::int64_t, void*) { + std::abort(); +} + +void APS5_VABI FreeCallback(void*, void*, LibreSslExData*, int, std::int64_t, void*) { + std::abort(); +} + +int APS5_VABI DupCallback(LibreSslExData*, LibreSslExData*, void*, int, std::int64_t, void*) { + std::abort(); +} + int NewSslIndex() { return SSL_get_ex_new_index_nid_postfix(7, nullptr, nullptr, nullptr, nullptr); } @@ -37,6 +50,11 @@ int main() { CRYPTO_cleanup_all_ex_data_nid_postfix(); Require(NewContextIndex() == 0, "cleanup can be repeated"); + CRYPTO_cleanup_all_ex_data_nid_postfix(); + int argument = 0; + Require(SSL_get_ex_new_index_nid_postfix(-5, &argument, NewCallback, DupCallback, FreeCallback) == 0, "typed SSL callbacks are accepted and stored"); + Require(SSL_CTX_get_ex_new_index_nid_postfix(9, &argument, NewCallback, DupCallback, FreeCallback) == 0, "typed SSL_CTX callbacks are accepted and stored"); + Require(NewSslIndex() == 1 && NewContextIndex() == 1, "indices with callbacks count like any other"); CRYPTO_cleanup_all_ex_data_nid_postfix(); std::vector threads; std::vector indices(64, -1); From d91daf899feccb0e698463e733907754267ab453 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:51:23 -0400 Subject: [PATCH 130/702] docs(libSceLibreSsl): shorten the ex_data debt entry Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceLibreSsl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 79dfb7335b..56333183ec 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -10,7 +10,7 @@ - [AES_set_encrypt_key, AES_set_decrypt_key, AES_encrypt, AES_decrypt, AES_ecb_encrypt, AES_cbc_encrypt, AES_cfb128_encrypt, AES_ctr128_encrypt](../../../core/libs/prx/libSceLibreSsl/src/Aes.cpp) (libSceLibreSsl) - follow LibreSSL's [aes.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/aes/aes.h) and [mode code](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/modes). Nothing is checked on the console. Key setup follows [LibreSSL 2.5.4's aes_core.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/aes_core.c), the version the `X509_verify_cert_error_string` entry names: a null pointer returns -1 and a key length other than 128, 192 or 256 bits returns -2 before the key is written, and only the 44, 52 or 60 words the key length needs are written. The `AES_KEY` schedule uses that file's generic C layout (big-endian words, `rounds` 10, 12 or 14). The 2.5.4 amd64 build uses [aes-x86_64](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/asm/aes-x86_64.pl) instead, which copies the key bytes without swapping, so each word is byte-reversed compared with the C layout (OpenSSL 3.0.13's x86-64 code gives the same schedules byte-reversed); the console's build options are unknown, so a schedule the title reads or fills is unverified. CTR leaves the last keystream block in `ecount_buf` even at a block boundary. Null pointers in the cipher and mode functions, other `rounds` values and a CFB or CTR position outside 0-15 throw - [ERR_get_error, ERR_peek_error, ERR_clear_error, ERR_remove_thread_state](../../../core/libs/prx/libSceLibreSsl/src/Error.cpp) (libSceLibreSsl) - no export of this library adds an error yet, so the queue stays empty for guest code. The per-thread queue of 16 slots holding 15 codes, its order and the `ERR_PACK` layout follow LibreSSL's [err.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.c) and [err.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.h); the module's LibreSSL version is unknown and nothing is checked on the console. The queue keeps no file, line or data, as no export reads them. `ERR_remove_thread_state` with a thread id throws: that version asserts a null id, while older versions remove the state of the thread the id names - [OCSP_response_status_str, OCSP_cert_status_str and OCSP_crl_reason_str](../../../core/libs/prx/libSceLibreSsl/src/Ocsp.cpp) (libSceLibreSsl) - the module's strings are not measured on the console. The tables follow LibreSSL [ocsp_prn.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/ocsp/ocsp_prn.c), unchanged from revision 1.8 (2015) to 1.12 (2025), with codes from `ocsp.h`; the argument is a 64-bit `long` and every value not in a table returns `(UNKNOWN)` -- [SSL_get_ex_new_index, SSL_CTX_get_ex_new_index, SSL_get_ex_data_X509_STORE_CTX_idx and CRYPTO_cleanup_all_ex_data](../../../core/libs/prx/libSceLibreSsl/src/ExData.cpp) (libSceLibreSsl) - unverified on a PS5. They follow LibreSSL 2.5.4's [ex_data.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/ex_data.c#L310-L406), [ssl_lib.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_lib.c#L2828-L2854) and [ssl_cert.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_cert.c#L133-L160): each class (SSL 1, SSL_CTX 2, X509_STORE_CTX 5) numbers its indices from 0, the verify-callback index is allocated in class 5 once and kept even after `CRYPTO_cleanup_all_ex_data`, which releases every class so numbering restarts at 0, and an allocation failure returns -1 with `ERR_R_MALLOC_FAILURE` queued. The `argl`, `argp` and callbacks are stored but never called, because no export creates an `SSL` or `SSL_CTX` and `SSL_get_ex_data`/`SSL_set_ex_data` still throw. +- [SSL_get_ex_new_index, SSL_CTX_get_ex_new_index, SSL_get_ex_data_X509_STORE_CTX_idx and CRYPTO_cleanup_all_ex_data](../../../core/libs/prx/libSceLibreSsl/src/ExData.cpp) (libSceLibreSsl) - unverified on a PS5. The stored `argl`, `argp` and callbacks are never called, because no export creates an `SSL` or `SSL_CTX` and `SSL_get_ex_data`/`SSL_set_ex_data` still throw. Index numbering follows LibreSSL 2.5.4's [ex_data.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/ex_data.c#L310-L406), [ssl_lib.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_lib.c#L2828-L2854) and [ssl_cert.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_cert.c#L133-L160): per class from 0, the verify-callback index allocated once and kept across `CRYPTO_cleanup_all_ex_data`, which restarts every class at 0. - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. From b3d00b129bef95b7466d9e192f70d1e593a1b777 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:23 +0000 Subject: [PATCH 131/702] docs(debt): drop the duplicate sceAudioOut2Set3DLatency entry --- docs/dev/debt/libSceAudioOut.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index d11e3678ee..29b89a1b30 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -116,7 +116,6 @@ - [sceAudioOutMasteringInit, sceAudioOutMasteringTerm and sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - non-zero flags, a null pointer and a params id other than 1 throw; a repeated Init, a Term without Init and a SetParam or GetState before Init are unknown. There is no mastering stage, so Init and Term keep no state and SetParam reads only the id. The signatures and the default params id 1 follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut.cs#L312-L344), not a console measurement. SharpProspero documents flag bit 0 as `SwapEqCompressor`, and shadPS4's PS4 stub [rejects any non-zero Init flags](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/audio/audioout.cpp#L956-L962) with 0x80260201 - [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the states id 1 and the meter offsets 32, 128, 240, 272 and 304 in a 336-byte struct follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Audio/AudioOutMastering.cs#L97-L110), not a console measurement. With no mastering stage it zeroes the compressor input RMS and gain, and the limiter input, output and gain peaks, and leaves the other bytes, including the descriptors the AudioOut2 call writes, untouched; zero is not known to be what the console reports for an idle chain. A null pointer and a states id other than 1 throw; the console error codes are unknown - [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without initialization validation or a chat service; repeated-call and no-session behavior therefore still depends on the missing service. -- [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls 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. - [sceAudioOut2ContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device counts the grains that `sceAudioOut2ContextAdvance` completed and no push has consumed as queued, together with the pushed grains beyond the 40 ms cushion, up to `queue_depth`. Whether the console counts them is unknown. Pushes that fill the cushion leave the level at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each of those pushes: PPSA23226's Wwise sink passes the available count to the sound engine as the number of frames to render, advances once per frame and pushes on a separate thread, and exceeded 16 completed grains. Counted, the completed grains keep such a title at most `queue_depth` grains ahead of its pushes. - [sceAudioOut2LoInit, sceAudioOut2LoContext* and sceAudioOut2LoPort* calls](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) have no low-latency path: each forwards to its `sceAudioOut2` counterpart, so grain and queue depth are the context's `num_grains` and `queue_depth`, and playback keeps the 40 ms cushion. [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L489-L545) only says each call takes the same handles and parameter blocks as its counterpart and drives the low-latency clock with a shallow queue and a small grain ([docs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/docs/audio.md#L662-L668)). Unverified: the console's grain and depth limits, its error for a handle of the other service (accepted here) and what `sceAudioOut2LoInit` returns when repeated (0 here). From 2e1f425e86e03cd04e893bbdd2b993c06b9bc2fe Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:23 +0000 Subject: [PATCH 132/702] docs(debt): drop the duplicate libSceAvPlayerStreaming pending exports entry --- docs/dev/debt/libSceAvPlayerStreaming.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/dev/debt/libSceAvPlayerStreaming.md b/docs/dev/debt/libSceAvPlayerStreaming.md index 4ef5238bac..3e5621fb3e 100644 --- a/docs/dev/debt/libSceAvPlayerStreaming.md +++ b/docs/dev/debt/libSceAvPlayerStreaming.md @@ -3,5 +3,3 @@ ### Unknown function info - [libSceAvPlayerStreaming pending exports](../../../core/libs/prx/libSceAvPlayerStreaming/Export.cpp) - 33 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayerStreaming.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - -- [libSceAvPlayerStreaming pending exports](../../../core/libs/prx/libSceAvPlayerStreaming/Export.cpp) - 33 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayerStreaming.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. From 801bfd80395fe36b08af5dca789a5cd0f196d649 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:23 +0000 Subject: [PATCH 133/702] docs(debt): drop the duplicate libSceBeisobmf pending exports entry --- docs/dev/debt/libSceBeisobmf.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/dev/debt/libSceBeisobmf.md b/docs/dev/debt/libSceBeisobmf.md index 643f09dd5e..44d43faa9f 100644 --- a/docs/dev/debt/libSceBeisobmf.md +++ b/docs/dev/debt/libSceBeisobmf.md @@ -3,5 +3,3 @@ ### Unknown function info - [libSceBeisobmf pending exports](../../../core/libs/prx/libSceBeisobmf/Export.cpp) - 18 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBeisobmf.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - -- [libSceBeisobmf pending exports](../../../core/libs/prx/libSceBeisobmf/Export.cpp) - 18 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBeisobmf.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. From cee32b9d091440e15f2d9548e5cf6a2a51826efa Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:23 +0000 Subject: [PATCH 134/702] docs(debt): drop the duplicate libSceBemp2sys pending exports entry --- docs/dev/debt/libSceBemp2sys.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/dev/debt/libSceBemp2sys.md b/docs/dev/debt/libSceBemp2sys.md index 1ca60eb890..558e91e63a 100644 --- a/docs/dev/debt/libSceBemp2sys.md +++ b/docs/dev/debt/libSceBemp2sys.md @@ -3,5 +3,3 @@ ### Unknown function info - [libSceBemp2sys pending exports](../../../core/libs/prx/libSceBemp2sys/Export.cpp) - 31 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBemp2sys.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - -- [libSceBemp2sys pending exports](../../../core/libs/prx/libSceBemp2sys/Export.cpp) - 31 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBemp2sys.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. From a80a247c5ef686294e5bb00cc90d15f132e0b1f1 Mon Sep 17 00:00:00 2001 From: Henryisdjfunkyho <197676650+Henryisdjfunkyho@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:36:10 -0700 Subject: [PATCH 135/702] docs(libkernel): cite SCE sleep cancellation evidence --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 27a1b2a86a..c4b21f34a2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -187,7 +187,7 @@ - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. PPSA99420 imports it - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest storage location for the life of the process; a guest that joins into a fresh location every time keeps every one. A handle copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over the original storage releases an object the copy still points at, and the copy reads freed memory. A detached thread is released when it ends, so joining it after it ended reads freed memory. A null entry crashed the payload and throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. PPSA34015 once stopped with `Joining guest thread: Access is denied` before the join and detach checks followed the console - [scePthreadCreate and scePthreadDetach, measured](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60): a name of 32 bytes or more returns `ENAMETOOLONG` (`0x8002003f`) without creating a thread, 31 is kept whole, and a null name gives an empty one; detaching a joined thread returns `ESRCH`; detaching a thread another thread is joining, or a detached one, returns `EINVAL`, as does joining a detached one; a second concurrent join returns `EOPNOTSUPP`; a null thread returns `EINVAL` from both; a null handle pointer to `scePthreadCreate` faulted and throws here. Whether the handle is written before the name check was not measured; here it is left alone -- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) is not verified on console and follows FreeBSD libthr: states/types 0/1 and 0/2, canceled value 1, and cleanup node layout. Requests act only in `scePthreadTestcancel`, condition-variable, `sem`/`scePthreadSem`, `scePthreadJoin` and sleep waits, and their POSIX names; file/socket calls are not cancellation points. SCE sleep calls check before and after host sleep and keep the GPU wake path for waits at most 1 ms; longer waits do not wake on cancellation. POSIX `usleep`, `nanosleep` and `_nanosleep` check before and after; only waits longer than 1 ms wake on enabled cancellation, while shorter waits keep the GPU wake path. POSIX `sleep` checks before and after its timed wait and wakes on enabled cancellation. Async cancellation acts like deferred except at the two set calls. +- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - PS5 kernel wakeup on mid-sleep cancellation remains unmeasured. [Firmware 13.42 disassembly and emulated checks](https://github.com/boykopovar/AnyPS5/pull/8916#issuecomment-6416760904) support the SCE cancellation checks before and after sleep. Long SCE waits still do not wake for cancellation here; six deferred tests pin that model until measured. POSIX `usleep`/`nanosleep`/`_nanosleep` wake on enabled cancellation only above 1 ms; shorter waits and short SCE waits retain the GPU wake path. POSIX `sleep` wakes on enabled cancellation. Requests act only in `scePthreadTestcancel`, condition-variable, semaphore, join and sleep waits and their POSIX names; file/socket calls are not cancellation points. States/types 0/1 and 0/2, canceled value 1 and cleanup layout follow FreeBSD libthr, not console measurements. Asynchronous cancellation acts like deferred except at the two set calls. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. 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. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. 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 - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified From 5d0a47107ee7198b640ad96eb821785d32176904 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Sat, 24 Oct 2026 20:51:23 +0530 Subject: [PATCH 136/702] feat(libSceAgc): emit ACB workload packets for complete, active and stream-inactive Firmware 4.03 shows the ACB and DCB variants share the same reserve-and-emit structure and differ only in a flag bit the command IR does not model, so mirror the DCB NOP-carried semantic payloads: stream and workload ids plus the 64-bit mask for complete and active, stream id only for inactive (9 dwords, matching sceAgcDriverGetSetWorkloadStreamInactivePacketSize). Validation is identical to the DCB twins: null buffer, unregistered or out-of-range stream, workload ids above 63, empty or oversized id lists and duplicate ids all return null. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- core/libs/prx/libSceAgc/Acb/src/Control.cpp | 49 ++++++++++++++++----- core/libs/prx/libSceAgc/tests/Commands.cpp | 28 +++++++++--- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index 1dd0314800..901f6b10e3 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -8,15 +8,24 @@ #include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" namespace { -std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) { - if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; - NotImplemented_nid_no_patch(function); - return nullptr; +constexpr std::uint32_t OpcodeNop = 0x10; +constexpr std::uint32_t ActivePacketWords = 18; +constexpr std::uint32_t CompletePacketWords = 12; +constexpr std::uint32_t InactivePacketWords = 9; +constexpr std::uint32_t MaxWorkloadId = 63; +constexpr std::uint32_t MaxActiveCount = 63; + +std::uint32_t* NewWorkloadPacket(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; } } @@ -83,18 +92,38 @@ std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t vide } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) { - (void)workloadIds; - (void)workloadCount; - return RejectUnregisteredWorkloadStream(buf, streamId, __func__); + if (buf == nullptr || workloadIds == nullptr || workloadCount == 0 || workloadCount > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + std::uint64_t mask = 0; + for (std::uint32_t index = 0; index < workloadCount; ++index) { + const auto id = workloadIds[index]; + if (id > MaxWorkloadId) return nullptr; + const std::uint64_t bit = std::uint64_t{1} << id; + if ((mask & bit) != 0) return nullptr; + mask |= bit; + } + auto* packet = NewWorkloadPacket(buf, ActivePacketWords, __func__); + packet[1] = streamId; + packet[2] = static_cast(mask); + packet[3] = static_cast(mask >> 32u); + return packet; } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) { - (void)workloadId; - return RejectUnregisteredWorkloadStream(buf, streamId, __func__); + if (buf == nullptr || workloadId > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + const std::uint64_t clearMask = ~(std::uint64_t{1} << workloadId); + auto* packet = NewWorkloadPacket(buf, CompletePacketWords, __func__); + packet[1] = streamId; + packet[2] = workloadId; + packet[3] = static_cast(clearMask); + packet[4] = static_cast(clearMask >> 32u); + return packet; } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) { - return RejectUnregisteredWorkloadStream(buf, streamId, __func__); + if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + auto* packet = NewWorkloadPacket(buf, InactivePacketWords, __func__); + packet[1] = streamId; + return packet; } uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) { diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index a9ec29ed76..24a6c0761a 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -294,16 +294,34 @@ void testWorkloadStreams() { check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); Storage registered; registered.words.fill(0xdeadbeefu); - const auto untouched = registered.words; expectFailure([&] { sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); }); - expectFailure([&] { sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); }); - expectFailure([&] { sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3); }); - expectFailure([&] { sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1); }); - check(registered.words == untouched && registered.buffer.cursor_up == registered.words.data(), "a workload call that threw modified the buffer"); + const auto* acbActive = sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); + check(acbActive == registered.words.data() && acbActive[0] == Agc::Command::Header(0x10, 18, 0) && acbActive[1] == 1 && acbActive[2] == 0x18 && acbActive[3] == 0, "ACB workloads active packet"); + for (std::uint32_t word = 4; word < 18; ++word) check(acbActive[word] == 0, "ACB workloads active packet tail is not zero"); + const auto* acbComplete = sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 33); + check(acbComplete == registered.words.data() + 18 && acbComplete[0] == Agc::Command::Header(0x10, 12, 0) && acbComplete[1] == 1 && acbComplete[2] == 33 && acbComplete[3] == 0xffffffffu && acbComplete[4] == 0xfffffffdu, "ACB workload complete packet"); + for (std::uint32_t word = 5; word < 12; ++word) check(acbComplete[word] == 0, "ACB workload complete packet tail is not zero"); + const auto* acbInactive = sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1); + check(acbInactive == registered.words.data() + 30 && acbInactive[0] == Agc::Command::Header(0x10, 9, 0) && acbInactive[1] == 1, "ACB workload stream inactive packet"); + for (std::uint32_t word = 2; word < 9; ++word) check(acbInactive[word] == 0, "ACB workload stream inactive packet tail is not zero"); + check(registered.buffer.cursor_up == registered.words.data() + 39, "incorrect ACB 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(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, duplicate, 2) == nullptr, "ACB duplicate workload ids accepted"); + check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, outOfRange, 1) == nullptr, "ACB workload id 64 accepted"); + check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, top, 1) != nullptr, "ACB workload id 63 rejected"); + check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 0) == nullptr, "ACB workload count 0 accepted"); + check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, nullptr, 2) == nullptr, "ACB null workload ids accepted"); + check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 0, ids, 2) == nullptr, "ACB stream id 0 accepted"); + check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 32, ids, 2) == nullptr, "ACB stream id 32 accepted"); + check(sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 64) == nullptr, "ACB workload complete id 64 accepted"); + check(sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr, "ACB null command buffer accepted for active"); check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr && sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr, "a workload call with a null command buffer for a registered stream returned a packet"); check(sceAgcDriverUnregisterWorkloadStream(1) == 0, "workload stream unregistration failed"); + check(sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3) == nullptr, "ACB workload complete packet after unregistration"); } void testIndexBuffer() { From 7f25d383c61264b6bde065a8be4e40aa9c5a2b75 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Sun, 25 Oct 2026 13:52:11 +0530 Subject: [PATCH 137/702] test(libSceAgc): pin workload NOP payload inertness and correct ACB debt --- core/libs/prx/libSceAgc/tests/Commands.cpp | 1 - core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 14 ++++++++++++++ docs/dev/debt/libSceAgc.md | 3 ++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 24a6c0761a..9f2cb75312 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -294,7 +294,6 @@ void testWorkloadStreams() { check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); Storage registered; registered.words.fill(0xdeadbeefu); - expectFailure([&] { sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); }); const auto* acbActive = sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); check(acbActive == registered.words.data() && acbActive[0] == Agc::Command::Header(0x10, 18, 0) && acbActive[1] == 1 && acbActive[2] == 0x18 && acbActive[3] == 0, "ACB workloads active packet"); for (std::uint32_t word = 4; word < 18; ++word) check(acbActive[word] == 0, "ACB workloads active packet tail is not zero"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index a95329f86d..bdcd9386b1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -108,6 +108,19 @@ void testNopPad() { AgcDriver::Pm4::Validate(largest, 0); } +void testWorkloadNopPayloadsIgnored() { + AgcDriver::QueueState state; + for (const auto& packet : { + makePacket(0x10, {1, 0x18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + makePacket(0x10, {1, 33, 0xffffffffu, 0xfffffffdu, 0, 0, 0, 0, 0, 0, 0}), + makePacket(0x10, {1, 0, 0, 0, 0, 0, 0, 0}), + }) execute(state, packet); + check(state.markers.empty() && state.indexBase == 0 && state.indexBufferSize == 0 && state.indexType == 0 && + state.instanceCount == 1 && state.predication.operation == 0 && state.predication.address == 0 && + state.shader.begin() == state.shader.end() && !state.savedContext && state.constantRam.empty(), + "a workload NOP payload reached queue state"); +} + void testRegisters() { AgcDriver::QueueState state; execute(state, makePacket(0x79, {0x242, 4})); @@ -2046,6 +2059,7 @@ int main(int argc, char** argv) { } testCatalog(); testNopPad(); + testWorkloadNopPayloadsIgnored(); testRewindPayloadBits(); testWriteChangedKeepsUntouchedBytes(); testWriteChangedStoresExactlyTheChangedRuns(); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 7dc8aa960d..99c5f1ecc4 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -43,7 +43,8 @@ - [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 - [sceAgcSetAmmSemaphoreMemory, sceAgcSetSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the checks, error codes and clearing are the firmware's, but two firmware steps are not reproduced. After its checks, the firmware validates the memory with libkernel `qSleeVC6lwI`: an ioctl `0xc018ac15` taking `{memory, size, 1}`. The AMM call always does this; the non-AMM call only for memory from 8 TiB up that ends below 15 TiB. The firmware then sends `{7: memory, 8: size, 0xb: 0 for AMM, 1 otherwise}` through sceAgcDriverPassInfoDownward. Neither step happens here, so a registration that the kernel or driver would refuse succeeds. A null memory with a non-zero size and a range that wraps the address space throw where the firmware would fault - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - 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. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. -- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting +- [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included); for a registered stream they write the same `IT_NOP` packets the DCB calls write: 18, 12 and 9 dwords carrying the stream id, the 64-bit active mask, and the workload id plus its clear mask respectively. The sizes match the firmware 4.03 packet-size helpers (`sceAgcDriverGetSetWorkloadsActivePacketSize`, `sceAgcDriverGetSetWorkloadCompletePacketSize` and `sceAgcDriverGetSetWorkloadStreamInactivePacketSize` in `libSceAgcDriver.sprx` return the same payloads the DCB helpers do, and the ACB emitter writes the identical packet with only the queue flag differing). The packets are inert on this host: `Pm4::Execute` returns on a NOP with operation 0 without reading the payload, so no workload state is tracked; the console's SET_UCONFIG_REG/WRITE_DATA/ATOMIC_MEM sequence in the entry below is what carries the semantics and is still not written. The console oracle in prosper measured only the null return for unregistered streams, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56)); the packet a registered stream gets is not measured. No ACB declaration was found; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384) says the ACB calls use the same driver builders as the DCB ones +- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - returns null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throws for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns and nothing written ([DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for it; its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)` signature is that of the DCB call in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting - [sceAgcDcbSetWorkload*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console's packets are not written: the executor has no ATOMIC_MEM or mask table. In `libSceAgc.sprx` (firmware 13.60, read from disassembly) all six write a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete) and a WRITE_DATA to 0xC343, and active and complete add an ATOMIC_MEM (header `0xC0071E00`) on the stream's 64-bit mask. [Measured on a PS5](https://gist.github.com/orezraey/befe7632b827421988b47a6c04e89e1d) (firmware 13.60): WRITE_DATA dwords 5 to 8 are `0x06010000`, `0xC343`, 0 and the mask's high half (0 for inactive); the header low byte is 0x04 on the DCB SET_UCONFIG_REG and WRITE_DATA and 0x00 on the ACB ones; the table was at `0xFE003A000 + 8 * stream`; the pointer passed to `sceAgcDriverRegisterWorkloadStream` is never written - [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 - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From aeef642b5f783bc39f0d083be68b6c2d0cb82ef7 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Mon, 26 Oct 2026 05:12:28 +0000 Subject: [PATCH 138/702] test(libSceAgc): check unset indexBufferSize in the NOP payload case unset-state comparison against 0 compiled but never held. Check the optional directly instead. --- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 2 +- docs/dev/debt/libSceAgc.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index bdcd9386b1..6361e9bfd0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -115,7 +115,7 @@ void testWorkloadNopPayloadsIgnored() { makePacket(0x10, {1, 33, 0xffffffffu, 0xfffffffdu, 0, 0, 0, 0, 0, 0, 0}), makePacket(0x10, {1, 0, 0, 0, 0, 0, 0, 0}), }) execute(state, packet); - check(state.markers.empty() && state.indexBase == 0 && state.indexBufferSize == 0 && state.indexType == 0 && + check(state.markers.empty() && state.indexBase == 0 && !state.indexBufferSize && state.indexType == 0 && state.instanceCount == 1 && state.predication.operation == 0 && state.predication.address == 0 && state.shader.begin() == state.shader.end() && !state.savedContext && state.constantRam.empty(), "a workload NOP payload reached queue state"); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 99c5f1ecc4..95826ceb0f 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -43,8 +43,8 @@ - [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 - [sceAgcSetAmmSemaphoreMemory, sceAgcSetSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the checks, error codes and clearing are the firmware's, but two firmware steps are not reproduced. After its checks, the firmware validates the memory with libkernel `qSleeVC6lwI`: an ioctl `0xc018ac15` taking `{memory, size, 1}`. The AMM call always does this; the non-AMM call only for memory from 8 TiB up that ends below 15 TiB. The firmware then sends `{7: memory, 8: size, 0xb: 0 for AMM, 1 otherwise}` through sceAgcDriverPassInfoDownward. Neither step happens here, so a registration that the kernel or driver would refuse succeeds. A null memory with a non-zero size and a range that wraps the address space throw where the firmware would fault - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - 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. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. -- [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included); for a registered stream they write the same `IT_NOP` packets the DCB calls write: 18, 12 and 9 dwords carrying the stream id, the 64-bit active mask, and the workload id plus its clear mask respectively. The sizes match the firmware 4.03 packet-size helpers (`sceAgcDriverGetSetWorkloadsActivePacketSize`, `sceAgcDriverGetSetWorkloadCompletePacketSize` and `sceAgcDriverGetSetWorkloadStreamInactivePacketSize` in `libSceAgcDriver.sprx` return the same payloads the DCB helpers do, and the ACB emitter writes the identical packet with only the queue flag differing). The packets are inert on this host: `Pm4::Execute` returns on a NOP with operation 0 without reading the payload, so no workload state is tracked; the console's SET_UCONFIG_REG/WRITE_DATA/ATOMIC_MEM sequence in the entry below is what carries the semantics and is still not written. The console oracle in prosper measured only the null return for unregistered streams, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56)); the packet a registered stream gets is not measured. No ACB declaration was found; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384) says the ACB calls use the same driver builders as the DCB ones -- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - returns null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throws for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns and nothing written ([DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for it; its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)` signature is that of the DCB call in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting +- [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or an unregistered stream; for a registered stream they write the same `IT_NOP` packets the DCB calls write: 18, 12 and 9 dwords carrying the stream id, the 64-bit active mask, the workload id plus its clear mask. The sizes match the firmware 4.03 `libSceAgcDriver.sprx` packet-size helpers, and the ACB emitter writes the identical packet with only the queue flag differing. The packets are inert here: `Pm4::Execute` returns on a NOP with operation 0 without reading the payload, so no workload state is tracked; the console's SET_UCONFIG_REG/WRITE_DATA/ATOMIC_MEM sequence below is not written. prosper's oracle measured only the null return for unregistered streams. No ACB declaration was found; OpenAGC says the ACB calls share the DCB driver builders +- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - returns null without writing to the buffer for a null command buffer or an unregistered stream, and throws for a registered stream. prosper's oracle measured only that null return, with no stream registered; the packet a registered stream gets is not measured. KytyPS5 has no body for it; its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence OpenAGC derives from the driver, so neither is taken. The `(buf, streamId)` signature is that of the DCB call in OpenAGC, which says the ACB calls use the same driver builders. `sceAgcDcbSetWorkloadsActive` checks the stream before reading the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 without faulting - [sceAgcDcbSetWorkload*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console's packets are not written: the executor has no ATOMIC_MEM or mask table. In `libSceAgc.sprx` (firmware 13.60, read from disassembly) all six write a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete) and a WRITE_DATA to 0xC343, and active and complete add an ATOMIC_MEM (header `0xC0071E00`) on the stream's 64-bit mask. [Measured on a PS5](https://gist.github.com/orezraey/befe7632b827421988b47a6c04e89e1d) (firmware 13.60): WRITE_DATA dwords 5 to 8 are `0x06010000`, `0xC343`, 0 and the mask's high half (0 for inactive); the header low byte is 0x04 on the DCB SET_UCONFIG_REG and WRITE_DATA and 0x00 on the ACB ones; the table was at `0xFE003A000 + 8 * stream`; the pointer passed to `sceAgcDriverRegisterWorkloadStream` is never written - [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 - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From 118d36d8f22889ca327ec879a5b0d9aa40e6d8b8 Mon Sep 17 00:00:00 2001 From: iAbdulrrahman Date: Mon, 26 Oct 2026 12:37:00 +0300 Subject: [PATCH 139/702] feat(libc): implement timingsafe_memcmp --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/Strings.cpp | 16 ++++++++++++++++ core/libs/tests/GuestStrings.cpp | 12 ++++++++++++ 3 files changed, 29 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 4373c3455a..68d8c564d8 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -927,6 +927,7 @@ void APS5_VABI bcopy_nid_postfix(const void* source, void* destination, size_t c void* APS5_VABI memcpy_nid_postfix(void* dest, const void* src, size_t n); void* APS5_VABI memmove_nid_postfix(void* dest, const void* src, size_t n); int APS5_VABI memcmp_nid_postfix(const void* s1, const void* s2, size_t n); +int APS5_VABI timingsafe_memcmp_nid_postfix(const void* b1, const void* b2, size_t len); const void* APS5_VABI memchr_nid_postfix(const void* s, int c, size_t n); void* APS5_VABI memrchr_nid_postfix(const void* s, int c, size_t n); void* APS5_VABI memccpy_nid_postfix(void* dest, const void* src, int c, size_t n); diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 508da2aa04..b3c9a8f376 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -923,4 +923,20 @@ int APS5_VABI wcscat_s_nid_postfix(char16_t* dest, size_t destsz, const char16_t return wcsncat_s_nid_postfix(dest, destsz, src, static_cast(-1)); } +int APS5_VABI timingsafe_memcmp_nid_postfix(const void* b1, const void* b2, size_t len) { + const auto* p1 = static_cast(b1); + const auto* p2 = static_cast(b2); + int result = 0; + int done = 0; + for (size_t i = 0; i < len; ++i) { + const int diff = static_cast(p1[i]) - static_cast(p2[i]); + const int isLess = diff < 0 ? -1 : 0; + const int isGreater = diff > 0 ? -1 : 0; + const int cmp = isLess - isGreater; + result |= cmp & ~done; + done |= isLess | isGreater; + } + return result; +} + } diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index c7ee8ee32e..e2aafeb81e 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -542,6 +542,18 @@ int main() { Require(std::memcmp(dest, "abc:", 4) == 0); Require(memccpy_nid_postfix(dest, "abcdef", ':', 6) == nullptr); Require(memccpy_nid_postfix(dest, "abcdef", ':', 0) == nullptr); + const char safeA[] = "abcdef"; + const char safeB[] = "abcdeg"; + const char safeC[] = "abcdef"; + Require(timingsafe_memcmp_nid_postfix(safeA, safeC, 6) == 0); + Require(timingsafe_memcmp_nid_postfix(safeA, safeB, 6) < 0); + Require(timingsafe_memcmp_nid_postfix(safeB, safeA, 6) > 0); + Require(timingsafe_memcmp_nid_postfix(safeA, safeB, 5) == 0); + Require(timingsafe_memcmp_nid_postfix(safeA, safeB, 0) == 0); + const unsigned char highA[] = {0x00, 0x80}; + const unsigned char highB[] = {0x00, 0x7f}; + Require(timingsafe_memcmp_nid_postfix(highA, highB, 2) > 0); + Require(timingsafe_memcmp_nid_postfix(highB, highA, 2) < 0); CheckSecureCopySizeLimits(); CheckSecureStringSizeLimits(); return CheckMemcpyOverlap() ? 0 : 1; From aecf949008fad9288a2b41cef78cfe69a1cfca9f Mon Sep 17 00:00:00 2001 From: iAbdulrrahman Date: Mon, 26 Oct 2026 14:15:00 +0300 Subject: [PATCH 140/702] fix(libc): make timingsafe_memcmp comparison strictly branchless --- core/libs/prx/libc/src/Strings.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index b3c9a8f376..f00948a088 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -927,14 +927,17 @@ int APS5_VABI timingsafe_memcmp_nid_postfix(const void* b1, const void* b2, size const auto* p1 = static_cast(b1); const auto* p2 = static_cast(b2); int result = 0; - int done = 0; + std::uint32_t done = 0; for (size_t i = 0; i < len; ++i) { - const int diff = static_cast(p1[i]) - static_cast(p2[i]); - const int isLess = diff < 0 ? -1 : 0; - const int isGreater = diff > 0 ? -1 : 0; - const int cmp = isLess - isGreater; - result |= cmp & ~done; - done |= isLess | isGreater; + const std::uint32_t a = p1[i]; + const std::uint32_t b = p2[i]; + const std::uint32_t lt = (a - b) >> 31; + const std::uint32_t gt = (b - a) >> 31; + const int cmp = static_cast(gt) - static_cast(lt); + const std::uint32_t d = a ^ b; + const std::uint32_t diffMask = static_cast(-static_cast((d | -d) >> 31)); + result |= cmp & static_cast(~done); + done |= diffMask; } return result; } From 631269f1e9fc7fc70da466b474efc502685bd2f2 Mon Sep 17 00:00:00 2001 From: iAbdulrrahman Date: Mon, 26 Oct 2026 12:51:21 +0300 Subject: [PATCH 141/702] feat(libScePosixForWebKit): implement arc4random_uniform --- core/libs/prx/libScePosixForWebKit/Export.cpp | 10 ++++++++++ .../libs/prx/libScePosixForWebKit/include/Api.hpp | 1 + core/libs/tests/GuestPosixRandom.cpp | 15 +++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index 14675b52d3..d1a3a2320e 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -193,6 +193,16 @@ std::uint32_t APS5_VABI arc4random_nid_postfix() { return value; } +std::uint32_t APS5_VABI arc4random_uniform_nid_postfix(std::uint32_t upper_bound) { + if (upper_bound < 2) return 0; + const std::uint32_t min = -upper_bound % upper_bound; + std::uint32_t value; + do { + value = arc4random_nid_postfix(); + } while (value < min); + return value % upper_bound; +} + int APS5_VABI __wrap_access(const char* path, int mode) { return access_nid_postfix(path, mode); } diff --git a/core/libs/prx/libScePosixForWebKit/include/Api.hpp b/core/libs/prx/libScePosixForWebKit/include/Api.hpp index 96834f0ad7..21562c5fcb 100644 --- a/core/libs/prx/libScePosixForWebKit/include/Api.hpp +++ b/core/libs/prx/libScePosixForWebKit/include/Api.hpp @@ -10,6 +10,7 @@ int APS5_VABI fnmatch_nid_postfix(const char* pattern, const char* string, int f 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); std::uint32_t APS5_VABI arc4random_nid_postfix(); +std::uint32_t APS5_VABI arc4random_uniform_nid_postfix(std::uint32_t upper_bound); int APS5_VABI __wrap_access(const char* path, int mode); int APS5_VABI __wrap_clock_gettime(int clockId, KernelTimespec* tp); int APS5_VABI __wrap_fchown(int descriptor, std::uint32_t owner, std::uint32_t group); diff --git a/core/libs/tests/GuestPosixRandom.cpp b/core/libs/tests/GuestPosixRandom.cpp index e4f7307064..e5564d285b 100644 --- a/core/libs/tests/GuestPosixRandom.cpp +++ b/core/libs/tests/GuestPosixRandom.cpp @@ -54,5 +54,20 @@ int main() { std::size_t repeats = 0; for (std::size_t index = 1; index < Draws; ++index) repeats += words[index] == words[index - 1]; Require(repeats < 16); + Require(arc4random_uniform_nid_postfix(0) == 0); + Require(arc4random_uniform_nid_postfix(1) == 0); + for (std::size_t index = 0; index < 1000; ++index) { + Require(arc4random_uniform_nid_postfix(7) < 7); + } + std::array uniformCounts{}; + for (std::size_t index = 0; index < 20000; ++index) { + const auto value = arc4random_uniform_nid_postfix(10); + Require(value < 10); + ++uniformCounts[value]; + } + for (const auto count : uniformCounts) Require(count > 1500 && count < 2500); + for (std::size_t index = 0; index < 100; ++index) { + Require(arc4random_uniform_nid_postfix(0x80000000u) < 0x80000000u); + } return 0; } From 8306ac91830b0ff363acafbe65c6acf0ffb8e592 Mon Sep 17 00:00:00 2001 From: iAbdulrrahman Date: Mon, 26 Oct 2026 14:33:00 +0300 Subject: [PATCH 142/702] docs(libScePosixForWebKit): update debt entry for arc4random_uniform --- docs/dev/debt/libScePosixForWebKit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index bf4a170fa8..5e0072be35 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -10,7 +10,7 @@ - [strcasestr](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - console locale behavior is unmeasured. It follows FreeBSD 11.4's [strcasestr.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/string/strcasestr.c) in the guest's supported C locale; other guest locales are not implemented. - [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`. - [__wrap_fchown, __wrap_link, __wrap_readlink, __wrap_statvfs and __wrap_symlink](../../../core/libs/prx/libScePosixForWebKit/Wrap.cpp) (libScePosixForWebKit) - whether the console's wrappers add sandbox checks or path rewriting for WebKit is unknown. Each takes the signature of the function it is named after, as GNU ld's `--wrap` does, and calls it: libkernel_sys's `fchown`, `link`, `readlink` and `symlink`, and libSceLibcInternal's `statvfs`, with their gaps -- [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 +- [arc4random, arc4random_uniform](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - `arc4random` 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). `arc4random_uniform(upper_bound)` returns a uniformly distributed value in `[0, upper_bound)` via rejection sampling over `arc4random`, returning `0` for upper bounds of 0 and 1 without drawing entropy. The console's generator and its reseeding are unknown; `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [pthread_setname_np](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) stores the guest thread name in libkernel's thread object, so `pthread_getname_np` returns it, and follows libkernel's `pthread_rename_np`: `EINVAL` (22) for a null thread or name, `ENAMETOOLONG` (63) from 32 bytes on, `0` otherwise, with the name truncated to 31 bytes when read back. It does not apply the Windows thread description or the job affinity that `scePthreadRename` sets, because those helpers are private to libkernel; the console's exact error numbers for this module are unverified - [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 - [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database From 6b31deaccf02c793aa69c25d38b58c2cd88b3b3d Mon Sep 17 00:00:00 2001 From: iAbdulrrahman Date: Mon, 26 Oct 2026 13:01:20 +0300 Subject: [PATCH 143/702] feat(libc): implement digittoint, isnumber, and ishexnumber --- core/libs/prx/libc/include/Api.hpp | 3 +++ core/libs/prx/libc/src/LocaleSupport.cpp | 8 ++++++++ core/libs/tests/GuestCharacter.cpp | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 4373c3455a..0691414d75 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -471,6 +471,9 @@ int APS5_VABI ispunct_nid_postfix(int c); int APS5_VABI isxdigit_nid_postfix(int c); int APS5_VABI isascii_nid_postfix(int c); int APS5_VABI toascii_nid_postfix(int c); +int APS5_VABI digittoint_nid_postfix(int c); +int APS5_VABI isnumber_nid_postfix(int c); +int APS5_VABI ishexnumber_nid_postfix(int c); int APS5_VABI toupper_nid_postfix(int c); int APS5_VABI tolower_nid_postfix(int c); void APS5_VABI _ZNSt8ios_baseD2Ev_nid_postfix(GuestLocale::IosBase* self); diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 075be23d4b..2fe471e204 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -364,6 +364,14 @@ int APS5_VABI ispunct_nid_postfix(int c) { return ClassifyCharacter(c, 0x08); } int APS5_VABI isxdigit_nid_postfix(int c) { return ClassifyCharacter(c, 0x01); } int APS5_VABI isascii_nid_postfix(int c) { return (c & ~0x7F) == 0; } int APS5_VABI toascii_nid_postfix(int c) { return c & 0x7F; } +int APS5_VABI digittoint_nid_postfix(int c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return 0; +} +int APS5_VABI isnumber_nid_postfix(int c) { return isdigit_nid_postfix(c); } +int APS5_VABI ishexnumber_nid_postfix(int c) { return isxdigit_nid_postfix(c); } int APS5_VABI toupper_nid_postfix(int c) { return ConvertCharacter(c, true); } int APS5_VABI tolower_nid_postfix(int c) { return ConvertCharacter(c, false); } diff --git a/core/libs/tests/GuestCharacter.cpp b/core/libs/tests/GuestCharacter.cpp index 7f900acea7..fa7a93f4ff 100644 --- a/core/libs/tests/GuestCharacter.cpp +++ b/core/libs/tests/GuestCharacter.cpp @@ -92,4 +92,12 @@ int main() { Require(!isascii_nid_postfix(256)); Require(toascii_nid_postfix(258) == 2); Require(toascii_nid_postfix(-1) == 0x7F); + for (int c = EOF; c <= 255; ++c) { + Require(bool(isnumber_nid_postfix(c)) == bool(std::isdigit(c))); + Require(bool(ishexnumber_nid_postfix(c)) == bool(std::isxdigit(c))); + if (c >= '0' && c <= '9') Require(digittoint_nid_postfix(c) == c - '0'); + else if (c >= 'a' && c <= 'f') Require(digittoint_nid_postfix(c) == c - 'a' + 10); + else if (c >= 'A' && c <= 'F') Require(digittoint_nid_postfix(c) == c - 'A' + 10); + else Require(digittoint_nid_postfix(c) == 0); + } } From 5abefb16acce4308b07f1a7096f3ac0a3308ca70 Mon Sep 17 00:00:00 2001 From: Michael Krumland-Dunning Date: Sat, 24 Oct 2026 16:38:07 -0700 Subject: [PATCH 144/702] fix(shader): ignore a runtime V#'s type for scalar buffer loads as RDNA2 does A scalar buffer load through a V# read at run time recorded an InvalidDescriptor fault when TYPE (word 3, bits 30-31) was nonzero, and the dispatch wrote nothing. Measured with the hardware oracle on RDNA2 (gfx1036), s_buffer_load ignores the whole fourth word: OOB_SELECT, TYPE, FORMAT, ADD_TID_ENABLE, INDEX_STRIDE, RESOURCE_LEVEL and DST_SEL change no result, so the scalar path no longer checks TYPE. Vector accesses keep the check. The debt entry now records the measurement and keeps gfx1013 and a null base as open. Co-Authored-By: Claude Opus 5.5 --- .../execution/ScalarBufferRuntimeWord3.cpp | 143 ++++++++++++++++++ .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 2 +- docs/dev/debt/shader.md | 2 +- 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ScalarBufferRuntimeWord3.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarBufferRuntimeWord3.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarBufferRuntimeWord3.cpp new file mode 100644 index 0000000000..86ca517d00 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarBufferRuntimeWord3.cpp @@ -0,0 +1,143 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Results = 4; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Records = 20; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0xe0300000, 0x80000200, 0xbf8c3f70, 0x7e1e0502, 0xbe8c0308, 0xbe8d0309, 0xbe8e030a, 0xf4200406, + 0xfa000004, 0xf4200446, 0xfa000010, 0xf4200486, 0xfa000014, 0xbf8cc07f, 0x7e060210, 0x7e080211, + 0x7e0a0212, 0x340c0084, 0xe0701000, 0x80010306, 0xe0701004, 0x80010406, 0xe0701008, 0x80010506, + 0xbf810000, +}; + +constexpr std::array Word3s{0x31016facu, 0x01016facu, 0x31000facu, 0x31816facu, 0x71016facu, 0xb1016facu, 0xf1016facu, 0x00000000u, 0xffffffffu}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "scalar buffer runtime word 3: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, false, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::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, GuestBlock& guest, std::uint32_t word3) { + Input.fill(word3); + Output.fill(0xdeadbeefu); + std::vector userData(12, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const auto data = BufferDescriptor(guest.Data(), Records); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(data.begin(), data.begin() + 3, userData.begin() + 8); + const std::span code(Code); + const std::span guestBytes(guest.Data(), 256); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}, {reinterpret_cast(guest.Data()), std::as_bytes(guestBytes)}}}; + 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; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t word3) { + constexpr std::array expected{0xa0000001u, 0xa0000004u, 0u}; + constexpr std::array names{"offset 0x4", "offset 0x10", "offset 0x14 past the records"}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t i = 0; i < expected.size(); ++i) { + const auto actual = Output[tid * Results + i]; + Require(actual == expected[i], "scalar buffer runtime word 3 " + Hex(word3) + ": lane " + std::to_string(tid) + " " + names[i] + " is " + Hex(actual) + ", expected " + Hex(expected[i])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + std::memset(guest.Data(), 0, BlockBytes); + for (std::uint32_t i = 0; i < 64; ++i) { + const std::uint32_t value = 0xa0000000u | i; + std::memcpy(guest.Data() + i * 4u, &value, sizeof(value)); + } + for (const auto word3 : Word3s) { + Run(*device, guest, word3); + Check(word3); + } + std::puts("scalar buffer runtime word 3 tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 01fe275817..ebd1dbb4c7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -92,7 +92,7 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, field(state, GcnGuestShaders() ? Binary(state, spv::OpBitwiseAnd, u32, context.Arg(*handle, 1u), ConstantU32(state, GcnBufferWord1Mask)) : context.Arg(*handle, 1u), 0u, 16u)), BdaConstant(state, 32u)); base = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, context.Arg(*handle, 0u)), high); } - auto invalid = nonzero(state, field(state, word3, 30u, 2u)); + auto invalid = memory.kind == ResourceKind::ScalarBuffer ? ConstantBool(state, false) : nonzero(state, field(state, word3, 30u, 2u)); auto index = ConstantU32(state, 0u); if (context.Memory(instruction).kind == ResourceKind::Buffer) { index = context.Arg(instruction, 1u); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e7aba24b21..c7ed1bf7f3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -193,7 +193,7 @@ - [SDWA source selects on `v_cvt_f32_ubyte0` to `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - the PS5's gfx1013 is not measured. Byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, as measured on RDNA2 (gfx1036, Ryzen 7 9800X3D integrated graphics, AMD Windows driver 32.0.21030.31) for all seven source selects with and without `SRC0_SEXT` over 16 sources (zero, all ones, each byte's and word's sign bit set alone, mixed patterns), wave32, IEEE 0, DX10_CLAMP 1, f32 denormals flushed, f16 denormals kept, round to nearest even, FP16_OVFL 0. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Shared loop latches](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a loop whose single latch holds instructions and is reached by one edge gets an empty continue block, so its continue construct is that one block. A latch that several `continue` paths share keeps its instructions in the continue construct, since isolating it would clone them into each path. When such a latch emits more than one SPIR-V block, as a memory or image access does, SPIRV-Cross (MoltenVK) folds the construct into a `for` increment and drops the header's `OpPhi` updates, so the loop never advances on Metal. No title is known to reach this case. -- [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. +- [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) - the PS5's gfx1013 is not measured, nor is a V# with a null base. Scalar loads use only the base address, stride and record count: the fourth word is normalized to zero for resources accessed exclusively by scalar loads, and a [V# read at run time](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) does not fault on its type for a scalar load. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31) with `s_buffer_load_dword` and `_dwordx4` through the [hardware oracle](../HW_ORACLE.md): every `OOB_SELECT`, `TYPE`, `FORMAT` 0, `ADD_TID_ENABLE`, `INDEX_STRIDE`, a zero and an all-ones fourth word read as 0x31016fac does; the range is `NUM_RECORDS` bytes with stride 0, else `STRIDE` × `NUM_RECORDS`, whatever `SWIZZLE_ENABLE`; a dword not wholly in range reads 0. Vector and mixed accesses retain descriptor validation. - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix - [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. - [Merged hull programs](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (registered binary type 3) run whole in the hull stage, one invocation per output control point. On the console hull lane i keeps local lane i's registers, which with fewer input than output control points stop matching the patch's control points from i = inputs on; here invocation i holds control point min(i, inputs - 1) of its own patch. A wave-mask branch (`s_cbranch_exec[n]z`, `s_cbranch_vcc[n]z`) tests the invocation's own lane, so it can skip a block, scalar instructions included, that the console runs for the wave; `v_readfirstlane_b32` returns the invocation's own value, not the first active lane's, and another lane's register throws. The register layout (`s3`, `v0`-`v5`, as for the separate halves) is not measured on hardware. s[0:1] hold the hull user-data address (SH 0x102/0x103) when a draw sets it and zero otherwise, so a program reading it unset faults at the null address. From b863f46e6bf1549c34a2bcd3a46b7e524f421a9d Mon Sep 17 00:00:00 2001 From: B-Howell <116104752+B-Howell@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:42:10 -0400 Subject: [PATCH 145/702] fix(ajm): bound resampler positions before integer conversion --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 5 +++-- core/libs/tests/GuestAjm.cpp | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 72284a8281..fa6809116b 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -623,6 +623,7 @@ std::size_t ResamplerHeld(const Instance& instance, std::size_t channels) { const auto& resampler = instance.resampler; if (resampler.frames.empty() || channels == 0) return 0; const std::size_t frames = resampler.frames.size() / channels; + if (resampler.position >= static_cast(frames)) return 0; const auto next = static_cast(resampler.position); return frames > next ? frames - next : 0; } @@ -645,8 +646,8 @@ void ResamplerProduce(Instance& instance, PcmOutputs& outputs, std::size_t chann const std::size_t available = resampler.frames.size() / channels; std::vector frame(frameBytes); while (outputs.Room() >= frameBytes) { + if (available <= 2 || resampler.position >= static_cast(available - 2)) break; const auto index = static_cast(resampler.position); - if (index + 2 >= available) break; const double t = resampler.position - static_cast(index); for (std::size_t channel = 0; channel < channels; ++channel) { const double p0 = resampler.frames[(index - 1) * channels + channel]; @@ -661,7 +662,7 @@ void ResamplerProduce(Instance& instance, PcmOutputs& outputs, std::size_t chann resampler.ratio = std::max(0.0f, resampler.ratio + resampler.change); } } - const auto index = std::min(static_cast(resampler.position), available); + const auto index = resampler.position >= static_cast(available) ? available : static_cast(resampler.position); if (index > 1) { resampler.frames.erase(resampler.frames.begin(), resampler.frames.begin() + static_cast((index - 1) * channels)); resampler.position -= static_cast(index - 1); diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 1aece55d9e..05019c0996 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -1630,6 +1631,15 @@ void TestResampleOpus(std::uint32_t context) { Require(decimated.afterFirstJob.ratio == 2.0f && decimated.afterFirstJob.samples == 2); Require(decimated.atEnd.result == 0 && decimated.atEnd.samples >= 0 && decimated.atEnd.samples <= 2); + for (const float ratio : {0x1p32f, 0x1p63f, std::nextafter(0x1p64f, 0.0f), 0x1p64f, std::numeric_limits::max()}) { + const std::uint32_t extreme = CreateOpus(context); + const auto skipped = Stream(context, extreme, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, ratio); + Require(skipped.pcm.size() == 2); + Require(skipped.pcm[0] == reference.pcm[0] && skipped.pcm[1] == reference.pcm[1]); + Require(skipped.atEnd.result == 0 && skipped.atEnd.samples == 0); + Require(sceAjmInstanceDestroy(context, extreme) == 0); + } + const std::uint32_t slower = CreateOpus(context); const auto interpolated = Stream(context, slower, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0.5f); RequireInterpolated(reference.pcm, interpolated.pcm, 2); From 0f871ec673ff118a0f865606b64a38436bcc168b Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:26:15 +0000 Subject: [PATCH 146/702] fix(libSceVideoOut): return INVALID_EVENT from the event getters for an event of another filter sceVideoOutGetEventId, sceVideoOutGetEventData and sceVideoOutGetEventCount threw for an event whose filter is not EVFILT_VIDEO_OUT, and sceVideoOutGetEventId also threw for a VideoOut id this library never registers. A title that asks for the id of each event in a queue that also holds other events stopped there. PPSA28180 compares the result with VIDEO_OUT_ERROR_INVALID_EVENT, as reported in #11376 (item 6). They return VIDEO_OUT_ERROR_INVALID_EVENT (0x8029000D) instead, before sceVideoOutGetEventData writes its output, as the PS4 libraries of shadPS4 and fpPS4 do. The id is compared in full, so an id above 32 bits is no longer read as a registered one. A null pointer still throws. The PS5 result is unverified and recorded in the libSceVideoOut debt. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 13 +++ core/libs/prx/libSceVideoOut/src/Event.cpp | 20 ++--- core/libs/tests/GuestVideoOutEventId.cpp | 88 +++++++++++++++++++++ docs/dev/debt/libSceVideoOut.md | 1 + 4 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 core/libs/tests/GuestVideoOutEventId.cpp diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 364cbd74e9..f3cc8ea5cb 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -240,6 +240,19 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING) + add_test_executable(guest_video_out_event_id_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutEventId.cpp) + target_include_directories(guest_video_out_event_id_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_event_id_tests PRIVATE libSceVideoOut libkernel libc) + configure_windows_unwind(guest_video_out_event_id_tests) + add_test(NAME guest_video_out_event_id COMMAND guest_video_out_event_id_tests) + set_tests_properties(guest_video_out_event_id PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_video_out_event_id PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(guest_video_out_hdr_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutHdr.cpp) target_include_directories(guest_video_out_hdr_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index dde7b34821..2c23c1f375 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -174,23 +174,17 @@ int APS5_VABI sceVideoOutGetEventId(const KernelEvent* ev) { if (ev == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } - if (ev->filter != EVFILT_VIDEO_OUT) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT"); - } - const int ident = static_cast(ev->ident); - if (ident != VIDEO_OUT_EVENT_FLIP && ident != VIDEO_OUT_EVENT_VBLANK && ident != VIDEO_OUT_EVENT_PRE_VBLANK_START && ident != VIDEO_OUT_EVENT_SET_MODE && ident != VIDEO_OUT_EVENT_VRR_STATUS) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT"); - } - return ident; + if (ev->filter != EVFILT_VIDEO_OUT) return VIDEO_OUT_ERROR_INVALID_EVENT; + const uintptr_t ident = ev->ident; + if (ident != VIDEO_OUT_EVENT_FLIP && ident != VIDEO_OUT_EVENT_VBLANK && ident != VIDEO_OUT_EVENT_PRE_VBLANK_START && ident != VIDEO_OUT_EVENT_SET_MODE && ident != VIDEO_OUT_EVENT_VRR_STATUS) return VIDEO_OUT_ERROR_INVALID_EVENT; + return static_cast(ident); } int APS5_VABI sceVideoOutGetEventData(const KernelEvent* ev, int64_t* data) { if (ev == nullptr || data == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } - if (ev->filter != EVFILT_VIDEO_OUT) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT"); - } + if (ev->filter != EVFILT_VIDEO_OUT) return VIDEO_OUT_ERROR_INVALID_EVENT; uint64_t eventData = static_cast(ev->data) >> 16u; if (ev->ident == static_cast(VIDEO_OUT_EVENT_FLIP) && (static_cast(ev->data) & 0x8000000000000000ULL) != 0) { eventData |= 0xffff000000000000ULL; @@ -203,9 +197,7 @@ int APS5_VABI sceVideoOutGetEventCount(const KernelEvent* ev) { if (ev == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } - if (ev->filter != EVFILT_VIDEO_OUT) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT"); - } + if (ev->filter != EVFILT_VIDEO_OUT) return VIDEO_OUT_ERROR_INVALID_EVENT; return static_cast((static_cast(ev->data) >> 12u) & 0xfu); } diff --git a/core/libs/tests/GuestVideoOutEventId.cpp b/core/libs/tests/GuestVideoOutEventId.cpp new file mode 100644 index 0000000000..73a9c3672b --- /dev/null +++ b/core/libs/tests/GuestVideoOutEventId.cpp @@ -0,0 +1,88 @@ +#include "prx/libSceVideoOut/include/Event.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int InvalidEvent = static_cast(0x8029000Du); +constexpr int FlipEvent = 0; +constexpr std::int16_t GraphicsCoreFilter = -14; +constexpr std::int64_t Unwritten = 0x5a5a; + +bool Returns(const std::string& what, const std::function& call, int expected) { + try { + const int result = call(); + if (result == expected) return true; + std::fprintf(stderr, "%s returned 0x%08x, expected 0x%08x\n", what.c_str(), static_cast(result), static_cast(expected)); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s threw: %s\n", what.c_str(), error.what()); + } + return false; +} + +bool Throws(const std::string& what, const std::function& call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + std::fprintf(stderr, "%s returned instead of throwing\n", what.c_str()); + return false; +} + +bool RejectsEventOfAnotherFilter(const std::string& what, const KernelEvent& event) { + std::int64_t data = Unwritten; + bool passed = Returns("sceVideoOutGetEventId of " + what, [&] { return sceVideoOutGetEventId(&event); }, InvalidEvent); + passed = Returns("sceVideoOutGetEventData of " + what, [&] { return sceVideoOutGetEventData(&event, &data); }, InvalidEvent) && passed; + passed = Returns("sceVideoOutGetEventCount of " + what, [&] { return sceVideoOutGetEventCount(&event); }, InvalidEvent) && passed; + if (data == Unwritten) return passed; + std::fprintf(stderr, "sceVideoOutGetEventData of %s wrote 0x%llx\n", what.c_str(), static_cast(data)); + return false; +} + +} + +int main() { + KernelEvent user{}; + user.filter = EVFILT_USER; + KernelEvent graphics{}; + graphics.filter = GraphicsCoreFilter; + graphics.data = 1; + KernelEvent unknown{}; + unknown.ident = 0x77; + unknown.filter = EVFILT_VIDEO_OUT; + KernelEvent wide{}; + wide.ident = 0x100000000; + wide.filter = EVFILT_VIDEO_OUT; + KernelEvent flip{}; + flip.ident = FlipEvent; + flip.filter = EVFILT_VIDEO_OUT; + flip.data = 0x12343000; + + bool passed = RejectsEventOfAnotherFilter("a user event", user); + passed = RejectsEventOfAnotherFilter("a graphics core event", graphics) && passed; + passed = Returns("sceVideoOutGetEventId of a VideoOut event with an unknown id", [&] { return sceVideoOutGetEventId(&unknown); }, InvalidEvent) && passed; + passed = Returns("sceVideoOutGetEventId of a VideoOut event with an id above 32 bits", [&] { return sceVideoOutGetEventId(&wide); }, InvalidEvent) && passed; + + std::int64_t data = Unwritten; + passed = Returns("sceVideoOutGetEventId of a flip event", [&] { return sceVideoOutGetEventId(&flip); }, FlipEvent) && passed; + passed = Returns("sceVideoOutGetEventCount of a flip event", [&] { return sceVideoOutGetEventCount(&flip); }, 3) && passed; + passed = Returns("sceVideoOutGetEventData of a flip event", [&] { return sceVideoOutGetEventData(&flip, &data); }, 0) && passed; + if (data != 0x1234) { + std::fprintf(stderr, "sceVideoOutGetEventData of a flip event wrote 0x%llx, expected 0x1234\n", static_cast(data)); + passed = false; + } + + passed = Throws("sceVideoOutGetEventId of a null event", [] { return sceVideoOutGetEventId(nullptr); }) && passed; + passed = Throws("sceVideoOutGetEventData of a null event", [&] { return sceVideoOutGetEventData(nullptr, &data); }) && passed; + passed = Throws("sceVideoOutGetEventData with a null data pointer", [&] { return sceVideoOutGetEventData(&flip, nullptr); }) && passed; + passed = Throws("sceVideoOutGetEventCount of a null event", [] { return sceVideoOutGetEventCount(nullptr); }) && passed; + if (!passed) return 1; + std::printf("guest VideoOut event getter tests passed\n"); + return 0; +} diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 97a7439d7e..6d0185c57a 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -31,6 +31,7 @@ ### Functional - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - opening a bus that is already open returns `SCE_VIDEO_OUT_ERROR_RESOURCE_BUSY` (`0x80290009`), as [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/videoout/driver.cpp#L53-L60) PS4 driver does for its main port; the PS5 result is unverified. PPSA99764 opens the main bus twice and carries on, presenting through the first handle +- [sceVideoOutGetEventId, sceVideoOutGetEventData, sceVideoOutGetEventCount](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - the PS5 result is unverified for an event of another filter, for which all three return `VIDEO_OUT_ERROR_INVALID_EVENT` (`0x8029000D`), and for a VideoOut id this library never registers, for which sceVideoOutGetEventId returns the same code. Both follow the PS4 libraries of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/cda4c7780adf59c84e2c70e080ac4f4a0ef7cdac/src/core/libraries/videoout/video_out.cpp#L187-L242) and [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscevideoout.pas#L719-L758); a null pointer throws here, where both return `VIDEO_OUT_ERROR_INVALID_ADDRESS`. #11376 (item 6) reports that PPSA28180 compares the result of sceVideoOutGetEventId for an event of another filter with `0x8029000D`; the call site was not checked here - [sceVideoOutOpen, bus 0x20](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA05666 opens the PS VR2 output as `sceVideoOutOpen(255, 0x20, 0, NULL)` after `sceHmd2ReprojectionEnableVrMode(0x2000F)`, and its own log calls the bus `SCE_VIDEO_OUT_BUS_TYPE_HMD2_MAIN`. The console answers `0x80290001` from a process that is not a game, with the headset connected and worn or not, for users 255 and 0 and for a null or zeroed open parameter, so that is returned here before the other argument checks; what a game gets with a headset attached is not measured. On the handle the title then calls `sceVideoOutRegisterBuffers2` with set 0, two buffers, a 4000x2040 attribute (tiling 0, pitch 0, pixel format `0x8100000022000000`), category 16 and a null option, where each `VideoOutBuffers.data` points at a 16-byte `{pointer, 2}` whose pointer is the structure it passed to `sceHmd2ReprojectionEnableMirroring`, holding per-eye 2000x2040 texture descriptors rather than frame memory; then `sceVideoOutConfigureOutput` and `sceVideoOutIsOutputSupported` with mode `0x2000F`, and the GPU waits on buffer 0 of that handle. None of that is modelled: the HMD output needs a VR backend. With the error code as its handle the title's next call, `sceVideoOutRegisterBuffers2`, throws here for the unknown handle; the console's answer to it is not measured. The TV bus answers `0x8029001E` to the HMD modes `0x20001` and `0x2000C` to `0x20010` on the console, as here - [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 - [sceVideoOutSubmitEopFlip](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) reserves a flip and returns an opaque DWORD token; presentation is driven by a matching graphics-queue `RELEASE_MEM` with `INT_SEL` 2 and that token in `INT_CTXID`. This five-argument ABI and packet use are inferred from PPSA21564's bundled AGC driver, whose original packet-emission routine is checked in an isolated host ABI probe. The console token encoding, output width beyond the observed DWORD, error policy and reservation capacity are unverified. Host tokens occupy `0x04000000..0x07ffffff`, never repeat within a process, and exhaustion throws. Closing cancels unsubmitted reservations; only live reservations match; consumed or cancelled context IDs execute as ordinary label writes without targeting a reopened port. Async-queue EOP flips throw. Buffer registration, flip modes, queue capacity and vsync presentation follow the existing host VideoOut model, not measured console behavior. From 787b6a672023d71529cc03cd3eb62f3cf93a586c Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Mon, 26 Oct 2026 13:31:00 -0300 Subject: [PATCH 147/702] fix(agc): sample cube descriptors with fewer than six slices as six faces --- .../Graphics/src/GuestTextureResource.cpp | 6 +- .../tests/GuestTextureResource.cpp | 11 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 5 +- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 22 +++ .../tests/execution/ImageSampleShortCube.cpp | 143 ++++++++++++++++++ .../IrMetadata/Resources.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 9 ++ .../Optimization/src/ResourceTracker.cpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 4 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 12 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleShortCube.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 7a7788c964..0f22b8c15b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -248,11 +248,7 @@ GuestTextureResource DecodeTextureResource(std::span words) case TextureDimension::kCube: Require(width == height, "guest cube texture descriptor is not square"); Require(baseArray <= depth, "guest cube texture descriptor has a base array past its last array slice"); - if (baseArray == depth) depth += 5u; - if (depth - baseArray + 1u < 6u) { - Require(false, "guest cube texture descriptor holds fewer than 6 array slices" + - describe() + " (base array " + std::to_string(baseArray) + ")"); - } + if (depth - baseArray < 5u) depth = baseArray + 5u; break; } diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index ab7db93b7a..22eb2c0eb2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -371,9 +371,14 @@ void RunGuestTextureResourceTests() { result = DecodeTextureResource(pack(cubeExtraSlice)); Require(result.baseArray == 0 && result.depthOrLastArray == 6, "a cube descriptor with a slice past its last whole cube lost a slice"); Require(DescribeSurface(result).imageLayers == 7, "a cube allocation with a slice past its last whole cube omitted it"); - Fields cubePartial = cube; - cubePartial.depth = 4; - rejectFields(cubePartial, "fewer than 6 array slices"); + for (const auto [first, last] : {std::pair{0u, 4u}, std::pair{0u, 1u}, std::pair{2u, 5u}, std::pair{6u, 10u}}) { + auto shortCube = cube; + shortCube.baseArray = first; + shortCube.depth = last; + result = DecodeTextureResource(pack(shortCube)); + Require(result.baseArray == first && result.depthOrLastArray == first + 5u, "a cube descriptor with fewer than six slices did not expose six faces from its base slice"); + Require(DescribeSurface(result).imageLayers == first + 6u, "a cube allocation with fewer than six slices omitted faces"); + } Fields multisampled = base; multisampled.typeRaw = 14; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 412903f163..69990df7d1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -78,6 +78,7 @@ #include #include #include +#include #include namespace { @@ -9615,8 +9616,8 @@ void singleCubeTests(const Device& device, Recorder& recorder) { context.detiler = &detiler; SampleProgram program(context, recorder, SAMPLE_ARRAY_SPV); const VkComponentMapping identity{}; - for (const auto first : {0u, 5u, 6u}) { - std::array words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, first | (first << 16u), 0, 0, 0}; + for (const auto [first, last] : {std::pair{0u, 0u}, std::pair{5u, 5u}, std::pair{6u, 6u}, std::pair{0u, 4u}, std::pair{2u, 5u}}) { + std::array words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, last | (first << 16u), 0, 0, 0}; auto resource = DecodeTextureResource(words); const auto geometry = DescribeSurface(resource); std::vector memory(static_cast(geometry.guestBytes) + 256); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index cc86441030..9b5d63a995 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -333,6 +333,7 @@ CompiledVariant sampleVariant() { image.flatLineCompatible = false; image.flatVolumeLineCompatible = false; image.dimensionQueryOnly = false; + image.sampledOnly = false; image.foldTexelOffsetsOnly = true; image.remappedDepth1D = true; image.byElements = 4; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2a31594300..2747b2201b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -8310,6 +8310,27 @@ void verifyCubeDepthCompareMode() { require(modes[selected].cube && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "cube color comparison did not select its emulated runtime mode"); } +void verifyShortCubeMode() { + using namespace ShaderRecompiler; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2DArray; + DescriptorValue descriptor{{0x02143930u, 71u << 20u, 0u, 0xb1b00facu, 4u, 0u, 0u, 0u}, 8u}; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + require(modes[ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)].cube, "a sampled cube with five slices did not select a cube runtime mode"); + image.sampledOnly = false; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "fewer than six slices is implemented only for sampling", "a loaded cube with five slices was accepted"); + for (const auto range : {0u, 5u, 5u | (5u << 16u), 6u}) { + descriptor.dwords[4] = range; + require(modes[ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)].cube, "a loaded cube with six faces or more was rejected"); + } + image.sampledOnly = true; + image.resourceClass = ImageResourceClass::Storage; + image.written = true; + descriptor.dwords[4] = 3u | (1u << 16u); + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, ResourceMaterializer::RuntimeImageModes(image))); }, "fewer than six slices is implemented only for sampling", "a stored cube with three slices was accepted"); +} + void verifyVolumeArrayLoadMode() { using namespace ShaderRecompiler; const DescriptorValue descriptor{{0x02466600u, 0x01400000u, 0u, 0xa0500204u, 1u, 0x00700000u, 0u, 0u}, 8u}; @@ -8731,6 +8752,7 @@ int main(int argc, char** argv) { verifyGuardedNullPointers(); verifyFmaskImageMode(); verifyCubeDepthCompareMode(); + verifyShortCubeMode(); verifyVolumeArrayLoadMode(); verifyArrayVolumeStoreMode(); verifyMultisamplePlaneReadMode(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleShortCube.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleShortCube.cpp new file mode 100644 index 0000000000..0a8c867276 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleShortCube.cpp @@ -0,0 +1,143 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 6; +constexpr std::uint32_t Words = 8; +constexpr std::uint32_t ResultWord = 4; +constexpr std::uint32_t Size = 64; +constexpr std::uint32_t Slices = 8; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t TypeCube = 11; +alignas(256) std::array Buffer{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array SampleCode{ + 0x34020085, 0xe03c1000, 0x80000201, 0xbf8c3f70, 0xf09c0f18, 0x00610502, 0xbf8c3f70, 0xe0781010, 0x80000501, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020085, 0xe03c1000, 0x80000201, 0xbf8c3f70, 0xf0001f18, 0x00010502, 0xbf8c3f70, 0xe0781010, 0x80000501, 0xbf810000, +}; + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +std::array TextureDescriptor(std::uint32_t baseArray, std::uint32_t depth) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (TypeCube << 28u), + depth | (baseArray << 16u), + 0u, + 0u, + 0u, + }; +} + +void FillTexture() { + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(0u, Slices - 1u))); + Require(surface.layers == Slices && surface.guestBytes <= Texels.size(), "image sample short cube: the slices do not fit the texel storage"); + for (std::uint32_t slice = 0; slice < Slices; ++slice) { + const auto value = Bits(static_cast(slice + 1u)); + for (std::uint64_t offset = 0; offset < surface.layerBytes; offset += 4u) { + std::copy_n(reinterpret_cast(&value), 4u, &Texels[surface.GuestLayerOffset(slice) + offset]); + } + } +} + +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::array BilinearSampler() { + return {0u, 0xfffu << 12u, (1u << 20u) | (1u << 22u) | (1u << 26u), 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t baseArray, std::uint32_t depth) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = TextureDescriptor(baseArray, depth); + const auto sampler = BilinearSampler(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + 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; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void CheckSamples(AgcDriver::VulkanDevice& device, std::uint32_t baseArray, std::uint32_t depth) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Buffer[tid * Words] = Bits(1.5f); + Buffer[tid * Words + 1u] = Bits(1.5f); + Buffer[tid * Words + 2u] = Bits(static_cast(tid)); + } + Run(device, SampleCode, baseArray, depth); + for (std::uint32_t face = 0; face < Threads; ++face) { + const float value = std::bit_cast(Buffer[face * Words + ResultWord]); + const float expected = static_cast(baseArray + face + 1u); + Require(value == expected, "image_sample_lz cube with BASE_ARRAY " + std::to_string(baseArray) + " and DEPTH " + std::to_string(depth) + ": face " + std::to_string(face) + " read " + std::to_string(value) + ", expected slice " + std::to_string(baseArray + face) + " (" + std::to_string(expected) + ")"); + } +} + +void CheckLoadRejected(AgcDriver::VulkanDevice& device) { + Buffer.fill(0u); + try { + Run(device, LoadCode, 0u, 4u); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find("fewer than six slices") != std::string_view::npos, std::string("image_load through a cube with five slices failed for another reason: ") + error.what()); + return; + } + throw std::runtime_error("image_load through a cube with five slices was accepted, but hardware loads stop at DEPTH"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(); + for (const auto [baseArray, depth] : {std::pair{0u, 5u}, std::pair{0u, 0u}, std::pair{0u, 4u}, std::pair{0u, 3u}, std::pair{0u, 1u}, std::pair{2u, 6u}, std::pair{2u, 5u}}) CheckSamples(*device, baseArray, depth); + CheckLoadRejected(*device); + std::puts("image sample short cube tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index e639277ea3..e8e9c1174e 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -97,6 +97,7 @@ struct ImageResource { bool flatLineCompatible = true; bool flatVolumeLineCompatible = true; bool dimensionQueryOnly = true; + bool sampledOnly = true; bool foldTexelOffsetsOnly = false; bool convertedSamples = false; bool remappedDepth1D = false; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 10202666a4..cb1577337d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -97,6 +97,12 @@ bool descriptorIsCube(const DescriptorValue& descriptor) { return rawImageType(descriptor) == ImageType::Cube; } +bool descriptorIsShortCube(const DescriptorValue& descriptor) { + const auto depth = descriptor.dwords[4] & 0x1fffu; + const auto baseArray = (descriptor.dwords[4] >> 16u) & 0x1fffu; + return descriptorIsCube(descriptor) && descriptor.dwordCount == 8u && baseArray < depth && depth - baseArray < 5u; +} + RdnaImageDimension descriptorDimension(const DescriptorValue& descriptor, RdnaImageDimension requested) { const bool wantArray = requested == RdnaImageDimension::Dim1DArray || requested == RdnaImageDimension::Dim2DArray || requested == RdnaImageDimension::Dim2DMsaaArray; switch (rawImageType(descriptor)) { @@ -230,6 +236,9 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag } const bool storage = base.resourceClass == ImageResourceClass::Storage; const bool table = base.table != NoTable; + if (decoded.cube && (storage || !base.sampledOnly) && descriptorIsShortCube(descriptor)) { + return rejectImage(PoisonReason::Dimension, "a cube descriptor with fewer than six slices is implemented only for sampling; loads, stores and dimension queries through it stop at its last slice"); + } decoded.fmask = IsFmaskTextureFormat(format); if (decoded.fmask && !base.fmaskCompatible) return rejectImage(PoisonReason::Fmask, "FMASK requires a direct 32-bit image load"); if (decoded.fmask && (storage || base.depthCompare || table)) { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index d0d0482387..0ac208645f 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1948,6 +1948,7 @@ private: image.flatLineCompatible = image.flatLineCompatible && flatLineAccess && !memory.imagePacked && memory.imageByElements == 0u; image.flatVolumeLineCompatible = image.flatVolumeLineCompatible && (flatLineLoad || flatLineSample) && !memory.imagePacked && memory.imageByElements == 0u; image.dimensionQueryOnly = image.dimensionQueryOnly && op == IrOpcode::ImageQueryDimensions; + image.sampledOnly = image.sampledOnly && ImageOpcodeInfoOf(op).needsSampler; if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { constexpr auto unsupported = RdnaImageSampleFlagLod | RdnaImageSampleFlagDerivative; if (op == IrOpcode::ImageGatherRaw || (memory.imageSampleFlags & unsupported) != 0u) image.emulatedCompare |= EmulatedCompare::Unsupported; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ce6863e337..c8681faaf8 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -141,7 +141,7 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 24, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 43, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 44, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); @@ -800,6 +800,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.flatLineCompatible); out.Value(image.flatVolumeLineCompatible); out.Value(image.dimensionQueryOnly); + out.Value(image.sampledOnly); out.Value(image.foldTexelOffsetsOnly); out.Value(image.convertedSamples); out.Value(image.remappedDepth1D); @@ -908,6 +909,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.flatLineCompatible); in.Value(image.flatVolumeLineCompatible); in.Value(image.dimensionQueryOnly); + in.Value(image.sampledOnly); in.Value(image.foldTexelOffsetsOnly); in.Value(image.convertedSamples); in.Value(image.remappedDepth1D); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index d4cf9e2f30..8eba383469 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 47; +inline constexpr std::uint32_t FormatVersion = 48; enum class LoadStatus { Loaded, diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index bf768f57f8..62d16aec78 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -180,7 +180,7 @@ - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), which neither the check that every word is cleared nor the store-back of each word in place needs, so such a slice takes the clear like slice 0. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`, the value the DB's depth test reads for a cleared tile (`HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`: the clear word "only clears HiZ", "the DB register contain[s] the full clear values"); the HiZ test before it, which rejects against each tile's own Min/Max Z range (the HTILE definitions in the same file), is not modelled, so a draw that HiZ would reject over a tile cleared with Min/Max Z 0 is tested against `DB_DEPTH_CLEAR` instead. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [RADV fill-memory kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver) - the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes RADV's 16-byte `fill_memory` compute kernel (`MatchClampedGlobalFill`: 11 GFX10.3 words, 64x1x1 groups, six user SGPRs with the address in `user[2..3]`, the largest store offset in `user[4]` and the value in `user[5]`; each thread stores 16 bytes at `min(thread * 16, user[4])`) as a fill of `min(threads * 16, user[4] + 16)` bytes. RADV initializes and fast-clears DCC keys and other metadata with it, so the key and metadata tracking sees such a fill where it is in the command stream; run as a dispatch, its global stores name no range, and a key fill was only seen once it had landed, after later writes to the surface. Only a 16-byte aligned address and largest offset are taken. RADV's 4-byte variant (fills below 16 bytes), other Mesa versions' encodings and other compilers still run as dispatches. The kernel words are the ones vkQuake's PS5 RADV build (Mesa 25.2) submitted; they are not compared with the NIR builder's output for other targets. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import: the largest aligned power-of-two block, from twice the import alignment up to the 8 MiB window, that holds the requested address and that they fill exactly, so a block only grows by doubling as its window fills. A block that would exceed the budget, would replace an import a build holds, or fails (a refused Windows alias, `vkAllocateMemory`) steps down to the next smaller block; a failed block is not tried again until one of its ranges changes. Ranges larger than the window are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound over adjacent ranges of any size from the first one's start (ending anywhere in the last range it crosses), joins the ranges it crosses into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer bound from its first piece's start crosses into a piece the joined import does not cover, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. -- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. +- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) of two to five slices are only sampled: the [materializer](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) rejects loads, stores and dimension queries through them, which on RDNA2 stop at `DEPTH` while the view holds six faces. Out-of-range cube-array sampling is unimplemented; PS5 equivalence is unmeasured. A range of fewer than six slices samples six faces from `BASE_ARRAY` (measured with `tools/hw-oracle` on gfx1036 for two to five slices, and on RX 6600, gfx1032, for equal `BASE_ARRAY` and `DEPTH`); one of six or more keeps every slice up to `DEPTH`. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube sampled through such a sampler throws. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Compressed FMASK reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) without an exact resident native owner throw. Native reads require eight fragments, one slice and mip, pipe-aligned DCC/CMASK, Z_64KB_X FMASK and unchanged, disjoint planes, each watched or in a host import the Linux write-watch probe compares (kept as a copy and compared at most once per collect epoch); an FMASK import awaiting its Windows deferred read is read first (`WatchedAfterSettle`). The host stores independent samples and exposes their expanded indices (`0x76543210`, AMD PAL `c5e80007`, `Gfx9Fmask::GetPackedExpandedValue`); console equivalence is unmeasured. Other layouts, standalone compressed decoding, guest publication and recipe reuse remain unsupported. Metadata writes invalidate the native view, including same-value stores, except a same-value CPU store to a compared plane. From fafd47501ae025c2ba8b219722a4d067d3c696cb Mon Sep 17 00:00:00 2001 From: Johan David Date: Sat, 24 Oct 2026 13:08:03 -0500 Subject: [PATCH 148/702] feat(agc): count why draws begin render passes The [draws] line said how many render passes the recorded draws began and how many continued one, not why a draw could not continue. Yakuza 0 (PPSA31327) begins about 1,000 passes per frame, one every 8 draws, each costing the pass's attachment store and load and two ALL_COMMANDS barriers; which of those breaks a change could avoid was not visible. Recorder::StartDrawPass returns the reason (PassBreak): the batch is new, a non-draw command, a label store, the sample counter or a sample segment closed the previous pass, the draw asked for a pass of its own, the pass key differs, or one of the PassHazard values. The batch remembers which caller closed its last pass (RenderPass::closedBy, set by endOpenRenderPass). Draw keeps the reason in the outcome, the [draws] line prints the non-zero reasons after "render passes N begun", and Graphics::DrawPassBreaks() returns the cumulative counts for the test. No behaviour changes. --- .../libSceAgcDriver/Graphics/include/Draw.hpp | 1 + .../Graphics/include/PassHazards.hpp | 18 ++ .../Graphics/include/Recorder.hpp | 4 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 22 ++- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 23 +-- .../tests/execution/PassBreakReasons.cpp | 164 ++++++++++++++++++ 6 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PassBreakReasons.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index ea02167400..14cf92c7f0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -124,6 +124,7 @@ struct DrawPassCounts { std::uint64_t storeFlushes = 0; }; std::array DrawPassTotals(); +std::array DrawPassBreaks(); const char* IndirectDrawPathName(IndirectDrawPath path); // Draw packets that drew nothing, for the [draws] line: Nothing (an empty count, or no color writes diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp index 298ca9c49e..f389ccc716 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp @@ -5,6 +5,7 @@ #define VK_NO_PROTOTYPES #endif #include +#include #include #include #include @@ -27,6 +28,23 @@ struct PassAccess { enum class PassHazard : std::uint8_t { None, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite }; +enum class PassBreak : std::uint8_t { Continued, NewBatch, Command, Label, Samples, Forced, Segment, Key, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite, Count }; +constexpr std::size_t PassBreakCount = static_cast(PassBreak::Count); +constexpr std::array PassBreakNames{"continued", "new batch", "command", "label", "samples", "forced", "segment", "key", "read after write", "write after write", "write after read", "image", "unknown read", "unknown write"}; + +constexpr PassBreak PassBreakFor(PassHazard hazard) { + switch (hazard) { + case PassHazard::ReadAfterWrite: return PassBreak::ReadAfterWrite; + case PassHazard::WriteAfterWrite: return PassBreak::WriteAfterWrite; + case PassHazard::WriteAfterRead: return PassBreak::WriteAfterRead; + case PassHazard::Image: return PassBreak::Image; + case PassHazard::UnknownRead: return PassBreak::UnknownRead; + case PassHazard::UnknownWrite: return PassBreak::UnknownWrite; + case PassHazard::None: break; + } + return PassBreak::Key; +} + class GuestRangeSet { public: void Insert(std::uint64_t begin, std::uint64_t end); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index f76e061983..d7ff4694a0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -67,6 +67,7 @@ public: VkCommandBuffer commands = VK_NULL_HANDLE; bool continued = false; bool barrier = true; + PassBreak reason = PassBreak::Continued; }; DrawPassStart StartDrawPass(std::uint64_t key, bool forced, bool fenced, const PassAccess& access); void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool fenced, const PassAccess& access, std::function afterPass = {}); @@ -668,6 +669,7 @@ private: std::uint64_t key = 0; std::uint32_t timing = NoTiming; std::function afterPass; + PassBreak closedBy = PassBreak::NewBatch; } renderPass; PassHazards passHazards; // A pass ended in this batch: Submit records the host-read barrier its draws left out. @@ -775,7 +777,7 @@ private: // form, toward the host only); returns whether the batch's host-read barrier was included. bool closeStoreRun(bool atSubmit = false); // Ends the render pass a draw left open (vkCmdEndRenderPass, the pass's trailing barrier). - void endOpenRenderPass(bool barrier = true); + void endOpenRenderPass(PassBreak by, bool barrier = true); // Records the queued key stores as one run (`forWriter`: before a command writing, reading or // labelling over one, not at Submit). void recordKeyStores(bool forWriter); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index f3dd7f8885..a8ff516131 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -565,6 +565,7 @@ struct DrawOutcome { // A recorded draw began a render pass of its own, or continued the previous draw's. bool passBegun = false; bool passContinued = false; + PassBreak passBreak = PassBreak::Continued; bool keyStoresFlushed = false; bool storesFlushed = false; bool addressDraw = false; @@ -641,6 +642,8 @@ struct DrawProfile { std::uint64_t passesBegun = 0; std::uint64_t passesContinued = 0; std::array passTotals{}; + std::array passBreaks{}; + std::array reportedPassBreaks{}; std::array reasons{}; // The CPU inside Graphics::Draw, split by outcome: a synchronous draw costs ten times a recorded // one, so one average would only show the synchronous population. @@ -711,6 +714,7 @@ void reportDraw(const std::array& us, const ShaderResources: if (outcome.recorded) ++(outcome.waited ? profile.waited : profile.recorded); if (outcome.completion) ++profile.completion; if (outcome.passBegun) ++profile.passesBegun; + if (outcome.passBegun) ++profile.passBreaks[static_cast(outcome.passBreak)]; if (outcome.passContinued) ++profile.passesContinued; if (outcome.recorded) { auto& totals = profile.passTotals[outcome.addressDraw ? 1 : 0]; @@ -740,8 +744,16 @@ void reportDraw(const std::array& us, const ShaderResources: const auto synchronous = profile.draws - profile.recorded - profile.waited; const auto average = [](double total, std::uint64_t count) { return count != 0 ? total / static_cast(count) : 0.0; }; char line[2048]; - int n = std::snprintf(line, sizeof(line), "[draws] %llu draws over 10 s (%llu recorded avg %.0f us, of them %llu with completion; %llu recorded then waited avg %.0f us; %llu synchronous avg %.0f us; render passes %llu begun, %llu draws continued one; waited or synchronous because:", static_cast(profile.draws), static_cast(profile.recorded), average(profile.recordedUs, profile.recorded), static_cast(profile.completion), static_cast(profile.waited), average(profile.waitedUs, profile.waited), static_cast(synchronous), average(profile.synchronousUs, synchronous), static_cast(profile.passesBegun), static_cast(profile.passesContinued)); + int n = std::snprintf(line, sizeof(line), "[draws] %llu draws over 10 s (%llu recorded avg %.0f us, of them %llu with completion; %llu recorded then waited avg %.0f us; %llu synchronous avg %.0f us; render passes %llu begun", static_cast(profile.draws), static_cast(profile.recorded), average(profile.recordedUs, profile.recorded), static_cast(profile.completion), static_cast(profile.waited), average(profile.waitedUs, profile.waited), static_cast(synchronous), average(profile.synchronousUs, synchronous), static_cast(profile.passesBegun)); const auto room = [&] { return n > 0 && static_cast(n) < sizeof(line); }; + bool listed = false; + for (std::size_t i = 1; i < PassBreakCount && room(); ++i) { + const auto count = profile.passBreaks[i] - profile.reportedPassBreaks[i]; + if (count == 0) continue; + n += std::snprintf(line + n, sizeof(line) - static_cast(n), "%s%s %llu", listed ? ", " : " (", PassBreakNames[i], static_cast(count)); + listed = true; + } + if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast(n), "%s, %llu draws continued one; waited or synchronous because:", listed ? ")" : "", static_cast(profile.passesContinued)); for (std::size_t i = SyncNone + 1; i < SyncCount && room(); ++i) { if (profile.reasons[i] != 0) n += std::snprintf(line + n, sizeof(line) - static_cast(n), " %s %llu", SyncReasonNames[i], static_cast(profile.reasons[i])); } @@ -790,6 +802,7 @@ void reportDraw(const std::array& us, const ShaderResources: profile.addressBuilds = 0; profile.draws = profile.recorded = profile.waited = profile.completion = 0; profile.passesBegun = profile.passesContinued = 0; + profile.reportedPassBreaks = profile.passBreaks; profile.reasons.fill(0); profile.recordedUs = profile.waitedUs = profile.synchronousUs = 0; profile.targetLookups = profile.slowLookups = 0; @@ -1123,6 +1136,12 @@ std::array DrawPassTotals() { return stats.passTotals; } +std::array DrawPassBreaks() { + auto& stats = Profile(); + std::lock_guard lock(stats.mutex); + return stats.passBreaks; +} + void CountDrawSkip(DrawSkip kind, double us) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -2252,6 +2271,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam const bool continued = start.continued; outcome.passContinued = continued; outcome.passBegun = !continued; + outcome.passBreak = start.reason; const auto commands = start.commands; if (!continued) recorder->PrepareSampleSlot(); if (capture) captureInputs(context, *recorder, commands, resources, drawBindings, record.targets.empty() || record.targets.front() == nullptr ? 0 : record.targets.front()->Descriptor().baseAddress); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 3abc0aec26..769bd0c080 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1439,7 +1439,7 @@ VkCommandBuffer Recorder::Commands(VkAccessFlags* coveredAccess) { // Work recorded after a draw's render pass or an inline store run must see their writes: the // pass's end and the run's trailing barrier go in first (a per-batch run waits for Submit, or // for a caller whose ranges overlap a queued store: FlushStores). - if (open->renderPass.open) endOpenRenderPass(); + if (open->renderPass.open) endOpenRenderPass(PassBreak::Command); if (open->run.open && !LabelRunsPerBatch()) closeStoreRun(); if (coveredAccess != nullptr) *coveredAccess = open->coveredAccess; open->coveredAccess = 0; @@ -1454,20 +1454,22 @@ Recorder::DrawPassStart Recorder::StartDrawPass(std::uint64_t key, bool forced, ensureOpen(); auto& pass = open->renderPass; if (!pass.open || forced || (countingSamples && segmentMode() && !segmentSlotReady(*open))) { + const auto reason = !pass.open ? pass.closedBy : forced ? PassBreak::Forced : PassBreak::Segment; const auto commands = Commands(); open->passHazards.Clear(); open->passHazards.BeginPass(); - return {commands, false, true}; + return {commands, false, true, reason}; } const auto hazard = open->passHazards.Check(access, pass.key == key); - if (hazard == PassHazard::None && pass.key == key) return {open->commands, true, false}; + if (hazard == PassHazard::None && pass.key == key) return {open->commands, true, false, PassBreak::Continued}; + const auto reason = PassBreakFor(hazard); const bool merge = MergeBarriers() && !fenced && !pass.fenced && !pass.afterPass; const bool elide = merge && hazard == PassHazard::None; - endOpenRenderPass(!merge); + endOpenRenderPass(reason, !merge); const auto commands = Commands(); if (!elide) open->passHazards.Clear(); open->passHazards.BeginPass(); - return {commands, false, !elide}; + return {commands, false, !elide, reason}; } void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool fenced, const PassAccess& access, std::function afterPass) { @@ -1487,7 +1489,7 @@ void Recorder::SealPassWriteOnly() { if (open != nullptr) open->passHazards.SealWriteOnly(); } -void Recorder::endOpenRenderPass(bool barrier) { +void Recorder::endOpenRenderPass(PassBreak by, bool barrier) { auto& pass = open->renderPass; if (open->segmentActive) endSegment(*open); EndRenderPass(context, open->commands); @@ -1502,6 +1504,7 @@ void Recorder::endOpenRenderPass(bool barrier) { open->coveredAccess = barrier ? VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT : 0; open->hostReadOwed = true; pass = {}; + pass.closedBy = by; } void Recorder::QueueKeyStore(VkBuffer buffer, VkDeviceSize first, VkDeviceSize last, std::shared_ptr seed, std::uint64_t begin, std::uint64_t end) { @@ -1642,7 +1645,7 @@ void Recorder::RecordStore(VkBuffer buffer, VkDeviceSize offset, std::spankeyStores.empty()) recordKeyStores(true); - if (open->renderPass.open) endOpenRenderPass(); + if (open->renderPass.open) endOpenRenderPass(PassBreak::Label); open->coveredAccess = 0; // Two transfers to the same bytes have no order of their own within the run. const auto overlapsRecorded = [&] { return std::any_of(run.recorded.begin(), run.recorded.end(), [&](const auto& store) { return std::get<0>(store) == buffer && offset < std::get<2>(store) && std::get<1>(store) < end; }); }; @@ -1715,7 +1718,7 @@ bool Recorder::closeStoreRun(bool atSubmit) { // land ahead of every store of the run, and the pass a draw left open ends. if (!open->keyStores.empty()) recordKeyStores(true); const bool passEndLast = open->renderPass.open && !open->renderPass.afterPass; - if (open->renderPass.open) endOpenRenderPass(); + if (open->renderPass.open) endOpenRenderPass(PassBreak::Label); const auto commands = open->commands; run.timing = beginTiming(ClassKey(CommandClass::LabelRun)); if (BarrierValidate()) { @@ -2108,7 +2111,7 @@ void Recorder::CountSamples() { gpuSampleCounter(); countingSamples = true; if (segmentMode() || open == nullptr || open->samples != VK_NULL_HANDLE) return; - if (open->renderPass.open) endOpenRenderPass(); + if (open->renderPass.open) endOpenRenderPass(PassBreak::Samples); beginSamples(*open); } @@ -3619,7 +3622,7 @@ void Recorder::Submit() { if (open == nullptr) return; GuestMemory::AssertGpuLockHeld("Recorder::Submit"); if (!open->keyStores.empty()) recordKeyStores(false); - if (open->renderPass.open) endOpenRenderPass(); + if (open->renderPass.open) endOpenRenderPass(PassBreak::NewBatch); const bool segments = segmentMode(); if (!segments) { endSamples(*open); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PassBreakReasons.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PassBreakReasons.cpp new file mode 100644 index 0000000000..91a050e469 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PassBreakReasons.cpp @@ -0,0 +1,164 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#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 DepthFastClearHarness::Compile; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Triangles; +using DepthFastClearHarness::Width; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Samples = 4; + +alignas(4096) std::array ColorA{}; +alignas(4096) std::array ColorB{}; +alignas(4096) std::array DepthA{}; +alignas(4096) std::array DepthB{}; +alignas(4096) std::array DepthC{}; + +struct Counts { + DrawPassCounts passes; + std::array breaks; +}; + +Counts Snapshot() { + return {DrawPassTotals()[0], DrawPassBreaks()}; +} + +void Expect(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("pass break reasons: " + what); +} + +std::string Describe(const Counts& before, const Counts& after) { + std::string text = std::to_string(after.passes.passesBegun - before.passes.passesBegun) + " begun, " + std::to_string(after.passes.passesContinued - before.passes.passesContinued) + " continued;"; + for (std::size_t i = 0; i < PassBreakCount; ++i) { + if (after.breaks[i] != before.breaks[i]) text += " " + std::string(PassBreakNames[i]) + " " + std::to_string(after.breaks[i] - before.breaks[i]); + } + return text; +} + +void ExpectBreak(const Counts& before, const Counts& after, PassBreak reason, const std::string& what) { + Expect(after.passes.passesBegun - before.passes.passesBegun == 1 && after.passes.passesContinued == before.passes.passesContinued, what + " did not begin exactly one pass: " + Describe(before, after)); + for (std::size_t i = 0; i < PassBreakCount; ++i) { + const std::uint64_t expected = i == static_cast(reason) ? 1u : 0u; + Expect(after.breaks[i] - before.breaks[i] == expected, what + " was not counted once as '" + PassBreakNames[static_cast(reason)] + "': " + Describe(before, after)); + } +} + +void Draw(const Context& context, AgcDriver::VulkanDevice& device, const Shaders& compiled, std::span color, std::uint64_t depth) { + const std::array shaders{{ + {ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(color.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, color.size(), 0xe4u}; + state.color.surfaceAddress = state.color.address; + state.color.samples = Samples; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = DepthTarget{depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false, Samples}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_LESS; + state.samples = Samples; + 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(Triangles().size()), 0, 1, 0, false}; + AgcDriver::Graphics::Draw(context, state, draw, shaders); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); +#endif + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if ((context.limits.framebufferColorSampleCounts & Samples) == 0 || (context.limits.framebufferDepthSampleCounts & Samples) == 0) { + std::puts("skipped, the device cannot render four-sample color and depth"); + return VulkanTestSkipped; + } + const auto shaders = Compile(*device); + device->WaitIdle(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + recorder.Activate(); + const auto depthA = reinterpret_cast(DepthA.data()); + const auto depthB = reinterpret_cast(DepthB.data()); + const auto depthC = reinterpret_cast(DepthC.data()); + auto before = Snapshot(); + Draw(context, *device, shaders, ColorA, depthA); + auto after = Snapshot(); + ExpectBreak(before, after, PassBreak::NewBatch, "the first draw of a batch"); + Draw(context, *device, shaders, ColorA, depthB); + Draw(context, *device, shaders, ColorB, depthC); + recorder.Submit(); + recorder.Sync(); + before = Snapshot(); + Draw(context, *device, shaders, ColorA, depthA); + after = Snapshot(); + ExpectBreak(before, after, PassBreak::NewBatch, "the first draw after a submit"); + before = after; + Draw(context, *device, shaders, ColorA, depthA); + after = Snapshot(); + Expect(after.passes.passesContinued - before.passes.passesContinued == 1 && after.breaks == before.breaks, "a draw with the open pass's key and no hazard did not continue it: " + Describe(before, after)); + recorder.Commands(); + before = Snapshot(); + Draw(context, *device, shaders, ColorA, depthA); + after = Snapshot(); + ExpectBreak(before, after, PassBreak::Command, "a draw after a non-draw command"); + before = after; + Draw(context, *device, shaders, ColorA, depthB); + after = Snapshot(); + ExpectBreak(before, after, PassBreak::Image, "a draw switching to a depth target the open pass does not touch while sharing its color target"); + before = after; + Draw(context, *device, shaders, ColorB, depthC); + after = Snapshot(); + ExpectBreak(before, after, PassBreak::Key, "a draw into targets the open pass does not touch"); + recorder.Submit(); + recorder.Sync(); + std::puts("pass break reasons tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From ee23428c554b3bfcc0a5e2c17b34e5d4501396b2 Mon Sep 17 00:00:00 2001 From: QRMOE <217609482+QRMOE@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:18:05 +0100 Subject: [PATCH 149/702] fix(agc): release a resident multisample depth before a GPU buffer may write over it Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestBufferMemory.cpp | 1 + .../execution/NativeMultisampleDepth.cpp | 88 ++++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index fcd09869de..ad69582374 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -5083,6 +5083,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { if (space != nullptr) completeSpaceWrites(); uploaded = true; for (const auto& region : regions) static_cast(FlushDepthPlanes(region.begin, static_cast(region.end - region.begin))); + for (const auto& [begin, end] : writes) RequireNoResidentDepthRead_nid_no_patch(begin, static_cast(end - begin)); for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); if (context.hostImportAlignment != 0) { auto& state = Imports(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 3d3531a577..ece5832117 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -52,7 +53,7 @@ using namespace AgcDriver::Graphics; class Device { public: - Device() { + explicit Device(bool importHost = false) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #elif defined(__APPLE__) @@ -102,11 +103,32 @@ public: queue.queueFamilyIndex = family; queue.queueCount = 1u; queue.pQueuePriorities = &priority; - VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + std::vector extensions; + if (importHost) { + const auto enumerateExtensions = function("vkEnumerateDeviceExtensionProperties"); + Check(enumerateExtensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + extensions.resize(count); + Check(enumerateExtensions(context.physical, nullptr, &count, extensions.data()), "vkEnumerateDeviceExtensionProperties"); + } + const auto supported = [&](const char* name) { return std::any_of(extensions.begin(), extensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; }); }; + const bool externalHost = supported(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) && supported(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + const std::array externalHostNames{VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME, VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME}; + VkPhysicalDeviceBufferDeviceAddressFeatures address{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES}; + address.bufferDeviceAddress = VK_TRUE; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, externalHost ? &address : nullptr}; device.queueCreateInfoCount = 1u; device.pQueueCreateInfos = &queue; device.pEnabledFeatures = &enabled; + device.enabledExtensionCount = externalHost ? static_cast(externalHostNames.size()) : 0u; + device.ppEnabledExtensionNames = externalHost ? externalHostNames.data() : nullptr; Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + if (externalHost) { + VkPhysicalDeviceExternalMemoryHostPropertiesEXT host{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &host}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &properties); + context.hostImportAlignment = host.minImportedHostPointerAlignment; + context.bufferDeviceAddress = true; + } context.deviceProc = function("vkGetDeviceProcAddr"); context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); @@ -1091,6 +1113,59 @@ void CheckUnwrittenGuestAccess(Context context, VkFormat format, std::uint32_t f std::printf("Native depth format=%u samples=%u: guest access retired the unwritten surface and refused the written one\n", static_cast(format), fragments); } +enum class BufferWrite { DepthPlane, Htile, Written }; + +void CheckBufferWrite(Context context, VkFormat format, std::uint32_t fragments, BufferWrite write) { + const VkExtent2D extent{5u, 3u}; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); + const auto metadataBytes = static_cast(HtileMetadataBytes(extent)); + TargetMemory memory(bytes, true), metadata(65536u, true), unrelated(65536u, true); + const auto guestByte = [](std::size_t offset) { return static_cast(offset * 5u + 1u); }; + for (std::size_t offset = 0u; offset < bytes; ++offset) memory.Data()[offset] = guestByte(offset); + std::memset(metadata.Data(), 0, metadataBytes); + DepthTarget target{memory.Address(), 0u, extent, format, 0.0f, 0u, metadata.Address(), false, fragments}; + target.depthSwizzle = 24u; + target.iterate256 = target.htilePipeAligned = target.htileDepthOnly = true; + Recorder recorder(context); + recorder.Activate(); + struct Cleanup { + Recorder& recorder; + Context context; + ~Cleanup() { recorder.Sync(); ClearCachedTextures(context.device); ClearDepthSurfaces(context.device); } + } cleanup{recorder, context}; + const auto resident = [&] { return HasResidentDepthGuards_nid_no_patch() && DepthSurfaceAt(target.address); }; + const auto upload = [&](std::uint64_t address, std::size_t length) { + GuestBufferMemory buffer(context); + buffer.AddWritable(address, length); + buffer.Upload(false); + Require(HostImportCovers(context, address, length), "the native depth buffer-write fixture was not bound as a host import"); + buffer.WriteBack(); + }; + Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE && resident(), "native depth buffer-write fixture has no resident owner"); + upload(unrelated.Address(), 256u); + Require(resident(), "an unrelated buffer write retired a native depth surface"); + if (write == BufferWrite::Written) { + NoteDepthSurfaceWrite(context, target); + TargetRefuses([&] { upload(target.address, 256u); }, "sample-preserving guest publication"); + Require(resident(), "a refused buffer write retired a written native depth surface"); + return; + } + if (write == BufferWrite::DepthPlane) upload(target.address + bytes - 256u, 256u); + else upload(target.htileAddress, metadataBytes); + Require(!resident(), "a buffer the GPU may write left an unwritten native depth surface resident over it"); + for (std::size_t offset = 0u; offset < bytes; ++offset) Require(memory.Data()[offset] == guestByte(offset), "releasing an unwritten native depth surface for a buffer write changed its guest bytes"); + Require(std::all_of(metadata.Data(), metadata.Data() + metadataBytes, [](std::byte value) { return value == std::byte{0}; }), "releasing an unwritten native depth surface for a buffer write changed its HTILE"); +} + +void CheckBufferWrites(Context context, VkFormat format, std::uint32_t fragments) { + if (context.hostImportAlignment == 0u) { + std::printf("skipped native depth buffer writes: the device does not import host memory\n"); + return; + } + for (const auto write : {BufferWrite::DepthPlane, BufferWrite::Htile, BufferWrite::Written}) CheckBufferWrite(context, format, fragments, write); + std::printf("Native depth format=%u samples=%u: a GPU buffer write over the depth or HTILE released the unwritten surface and refused the written one\n", static_cast(format), fragments); +} + void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); @@ -1440,6 +1515,15 @@ int main(int argc, char** argv) { CheckUnwrittenGuestAccess(context, format, samples); } } + { + const Device importing(true); + const auto importContext = importing.GetContext(); + for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT}) { + for (const auto samples : {2u, 4u, 8u}) { + if (DepthSupported(importContext, format, samples)) CheckBufferWrites(importContext, format, samples); + } + } + } CheckEligibility(context); std::printf("Native multisampled depth cases checked=%u unavailable=%u\n", checked, unavailable); return 0; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index bf768f57f8..aab16017e3 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -178,6 +178,7 @@ - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a 3D or array color target throw; base mip 0 of a 2D mipmapped color target is supported (offset follows AMD Addrlib `Gfx10Lib::HwlComputeCmaskInfo` reverse mip-chain ordering preceding base mip 0, unverified on PS5 hardware; mipmapped targets with 8 or 16 bpp throw until `dsMipmapHtileFix` mip-tail shrinkage is modelled). A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. A clear hits a surface only when its range covers that surface's whole HTILE, which need not start where the clear does. Fast clears that other writers leave in HTILE are taken at bind as the next entry describes. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), which neither the check that every word is cleared nor the store-back of each word in place needs, so such a slice takes the clear like slice 0. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`, the value the DB's depth test reads for a cleared tile (`HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`: the clear word "only clears HiZ", "the DB register contain[s] the full clear values"); the HiZ test before it, which rejects against each tile's own Min/Max Z range (the HTILE definitions in the same file), is not modelled, so a draw that HiZ would reject over a tile cleared with Min/Max Z 0 is tested against `DB_DEPTH_CLEAR` instead. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. +- [Native multisampled depth under GPU buffers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a buffer range a shader may write that overlaps the depth or HTILE range of a resident native multisampled depth is handled at upload like a guest read, before the GPU runs: a surface the host has not written is retired, and a written one goes through guest publication. A buffer bound read-only is not, so a shader reading the depth's memory through it sees the guest bytes from before the surface's draws. - [RADV fill-memory kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver) - the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes RADV's 16-byte `fill_memory` compute kernel (`MatchClampedGlobalFill`: 11 GFX10.3 words, 64x1x1 groups, six user SGPRs with the address in `user[2..3]`, the largest store offset in `user[4]` and the value in `user[5]`; each thread stores 16 bytes at `min(thread * 16, user[4])`) as a fill of `min(threads * 16, user[4] + 16)` bytes. RADV initializes and fast-clears DCC keys and other metadata with it, so the key and metadata tracking sees such a fill where it is in the command stream; run as a dispatch, its global stores name no range, and a key fill was only seen once it had landed, after later writes to the surface. Only a 16-byte aligned address and largest offset are taken. RADV's 4-byte variant (fills below 16 bytes), other Mesa versions' encodings and other compilers still run as dispatches. The kernel words are the ones vkQuake's PS5 RADV build (Mesa 25.2) submitted; they are not compared with the NIR builder's output for other targets. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import: the largest aligned power-of-two block, from twice the import alignment up to the 8 MiB window, that holds the requested address and that they fill exactly, so a block only grows by doubling as its window fills. A block that would exceed the budget, would replace an import a build holds, or fails (a refused Windows alias, `vkAllocateMemory`) steps down to the next smaller block; a failed block is not tried again until one of its ranges changes. Ranges larger than the window are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound over adjacent ranges of any size from the first one's start (ending anywhere in the last range it crosses), joins the ranges it crosses into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer bound from its first piece's start crosses into a piece the joined import does not cover, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. From 63e01939e4c701b0585cb5a3b75a247d93095ba3 Mon Sep 17 00:00:00 2001 From: QRMOE <217609482+QRMOE@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:23:47 +0100 Subject: [PATCH 150/702] test(agc): enable buffer device addresses only when the device supports them Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/NativeMultisampleDepth.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index ece5832117..b796b1c8b2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -111,10 +111,17 @@ public: Check(enumerateExtensions(context.physical, nullptr, &count, extensions.data()), "vkEnumerateDeviceExtensionProperties"); } const auto supported = [&](const char* name) { return std::any_of(extensions.begin(), extensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; }); }; - const bool externalHost = supported(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) && supported(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + const bool externalHostExtensions = supported(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) && supported(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); const std::array externalHostNames{VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME, VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME}; VkPhysicalDeviceBufferDeviceAddressFeatures address{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES}; - address.bufferDeviceAddress = VK_TRUE; + if (externalHostExtensions) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &address}; + function("vkGetPhysicalDeviceFeatures2")(context.physical, &features); + } + const bool externalHost = externalHostExtensions && address.bufferDeviceAddress == VK_TRUE; + address.pNext = nullptr; + address.bufferDeviceAddressCaptureReplay = VK_FALSE; + address.bufferDeviceAddressMultiDevice = VK_FALSE; VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, externalHost ? &address : nullptr}; device.queueCreateInfoCount = 1u; device.pQueueCreateInfos = &queue; @@ -1159,7 +1166,7 @@ void CheckBufferWrite(Context context, VkFormat format, std::uint32_t fragments, void CheckBufferWrites(Context context, VkFormat format, std::uint32_t fragments) { if (context.hostImportAlignment == 0u) { - std::printf("skipped native depth buffer writes: the device does not import host memory\n"); + std::printf("skipped native depth buffer writes: the device lacks VK_EXT_external_memory_host, VK_KHR_buffer_device_address or the bufferDeviceAddress feature\n"); return; } for (const auto write : {BufferWrite::DepthPlane, BufferWrite::Htile, BufferWrite::Written}) CheckBufferWrite(context, format, fragments, write); From 946de281e144b6046582bc181ad38ff65d17d8d2 Mon Sep 17 00:00:00 2001 From: QRMOE <217609482+QRMOE@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:45:06 +0100 Subject: [PATCH 151/702] test(agc): match the guest publication refusal of a written native depth Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index b796b1c8b2..8f652a790d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -1153,7 +1153,7 @@ void CheckBufferWrite(Context context, VkFormat format, std::uint32_t fragments, Require(resident(), "an unrelated buffer write retired a native depth surface"); if (write == BufferWrite::Written) { NoteDepthSurfaceWrite(context, target); - TargetRefuses([&] { upload(target.address, 256u); }, "sample-preserving guest publication"); + TargetRefuses([&] { upload(target.address, 256u); }, "guest publication"); Require(resident(), "a refused buffer write retired a written native depth surface"); return; } From 905b15f25ca2ba77b421afbecc0816990994a6b1 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:10:00 +0100 Subject: [PATCH 152/702] fix(libc): compute sinh with FreeBSD msun's algorithm sinh_nid_postfix called the host libm's sinh. It now follows the branches and thresholds of FreeBSD msun's e_sinh.c (release/11.4.0): below 22 through expm1, between 22 and ln(DBL_MAX) through exp, and up to the overflow threshold through exp scaled as in k_exp.c's __ldexp_exp. Below 22 the results are bit-identical to msun; from 22 on they depend on the exp this file provides. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/Math.cpp | 29 ++++++++++++++++++++++++++++- docs/dev/debt/libc.md | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 91e8efa251..fc59683ace 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1009,7 +1009,34 @@ void APS5_VABI sincosf_nid_postfix(float x, float* sinp, float* cosp) { double APS5_VABI sin_nid_postfix(double x) { return std::sin(x); } double APS5_VABI cos_nid_postfix(double x) { return std::cos(x); } -double APS5_VABI sinh_nid_postfix(double x) { return std::sinh(x); } +static double SinhLdexpExpHalf(double x) { + constexpr std::uint32_t k = 1799; + constexpr double kln2 = 1246.97177782734161156; + const double expX = exp_nid_postfix(x - kln2); + const std::uint64_t bits = std::bit_cast(expX); + const std::uint32_t hx = static_cast(bits >> 32); + const int expt = -1 + static_cast(hx >> 20) - (0x3ff + 1023) + static_cast(k); + const std::uint32_t scaledHigh = (hx & 0xfffff) | ((0x3ff + 1023) << 20); + const double scaled = std::bit_cast((bits & 0xffffffffull) | (static_cast(scaledHigh) << 32)); + return scaled * std::bit_cast(static_cast(0x3ff + expt) << 52); +} +double APS5_VABI sinh_nid_postfix(double x) { + const double one = 1.0; + const double shuge = 1.0e307; + const std::int32_t jx = static_cast(std::bit_cast(x) >> 32); + const std::int32_t ix = jx & 0x7fffffff; + if (ix >= 0x7ff00000) return x + x; + const double h = jx < 0 ? -0.5 : 0.5; + if (ix < 0x40360000) { + if (ix < 0x3e300000 && shuge + x > one) return x; + const double t = Expm1(std::fabs(x)); + if (ix < 0x3ff00000) return h * (2.0 * t - t * t / (t + one)); + return h * (t + t / (t + one)); + } + if (ix < 0x40862e42) return h * exp_nid_postfix(std::fabs(x)); + if (ix <= 0x408633ce) return h * 2.0 * SinhLdexpExpHalf(std::fabs(x)); + return x * shuge; +} double APS5_VABI cosh_nid_postfix(double x) { return std::cosh(x); } float APS5_VABI coshf_nid_postfix(float x) { const std::uint32_t magnitude = std::bit_cast(x) & 0x7fffffffu; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ae8a6c9018..c27395cd75 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -146,6 +146,7 @@ - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows and macOS omit per-interface AF_LINK entries and interfaces without an address; on Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports an empty name without querying the host machine name, as a payload process read on a PS5 (firmware 13.60): 0 with only the terminator for a 256-byte and a 1-byte buffer. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); whether a title process or a console with a configured name reads another name, and the integration with `sysctl` and name resolution, are unverified and not implemented. +- [sinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_sinh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_sinh.c) over this file's msun `Expm1` for |x| < 22, where it gives msun's bits (0 of 2,000,000 random arguments in [-20, 20] differ from a locally built msun on a Windows host). From 22 on it calls this file's `exp`, which is still the host's, so those results can differ from msun in the last bit until `exp` follows msun; the last step before the overflow threshold scales that `exp` as msun's `__ldexp_exp` does. It sets no `errno`, as msun's does not. It was compared with msun built on the host, not with a console: the titles' `libc.prx` and the system `libSceLibcInternal` are unmeasured. - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, up to six bytes per sequence, with no surrogate or 0x10FFFF check when encoding. The state holds the value in its first eight bytes and the expected byte count (0x10 for a pending low surrogate, 1 for a stored high one) as 16 bits at 0xA. `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (13.60) the system library's default locale converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; a title's bundled `libc.prx` does too until `setlocale` installs a table ([disassembly](https://github.com/boykopovar/AnyPS5/pull/8855#issuecomment-6335267758)) From 0567c826e7fb89b1b4de97bb1e07c21a8bd0133a Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:10:03 +0100 Subject: [PATCH 153/702] test(libc): pin sinh bits below 22 to FreeBSD msun's results Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestMath.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index ea2219eb63..6eb8baf7a8 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -651,6 +651,36 @@ static void CheckTanhConsoleBits() { } } +static void CheckSinhMsunBits() { + constexpr std::uint64_t cases[][2] = { + {0x0000000000000000ull, 0x0000000000000000ull}, {0x8000000000000000ull, 0x8000000000000000ull}, + {0x3e2fffffffffffffull, 0x3e2fffffffffffffull}, {0x3e30000000000000ull, 0x3e30000000000000ull}, + {0xbe30000000000000ull, 0xbe30000000000000ull}, {0x3ff0000000000000ull, 0x3ff2cd9fc44eb982ull}, + {0xbff0000000000000ull, 0xbff2cd9fc44eb982ull}, {0x3fefffffffffffffull, 0x3ff2cd9fc44eb982ull}, + {0x4035ffffffffffffull, 0x41dab5adb9c435e5ull}, {0xc035ffffffffffffull, 0xc1dab5adb9c435e5ull}, + {0x0000000000000001ull, 0x0000000000000001ull}, {0x7ff0000000000000ull, 0x7ff0000000000000ull}, + {0xfff0000000000000ull, 0xfff0000000000000ull}, + {0xbfd6a0128184d680ull, 0xbfd719794d320780ull}, {0xbfbdff8696a49900ull, 0xbfbe111cd84473d7ull}, + {0xbfd6d8bb6b4dca00ull, 0xbfd755bf29c06d04ull}, {0xbfcd8f04fd096d80ull, 0xbfcdd2743062c130ull}, + {0xbfdaafb23b684980ull, 0xbfdb7764637ce2b1ull}, {0x4030fc47aee85be4ull, 0x4166b421a358e13dull}, + {0x3ff0ed5ed6534a20ull, 0x3ff444323b77d92aull}, {0x402e263f514d91a0ull, 0x413ae005c1e986f0ull}, + {0x402dc690717af4e8ull, 0x41364b45352a81bdull}, {0x403270a47f0a53e2ull, 0x41884ea3c3c84121ull}, + {0xc012535983b8e876ull, 0xc04868fc3820124dull}, {0xc0304bcc37b4b009ull, 0xc156ca0b8a37acd3ull}, + {0xc01a5966b21f1396ull, 0xc076ae9222cc12f7ull}, {0xc02ddcff626eb852ull, 0xc1374ae500e7533full}, + {0xc020ed9a3019cd6eull, 0xc0a2854d2aab8a5full}, + {0x4035dab76b89b207ull, 0x41d716f7bb9c87dbull}, {0x4035d187a12d7d23ull, 0x41d6469c55754089ull}, + {0x403527ff77fdabb1ull, 0x41c6f9a671b8ec60ull}, {0x4035deab000d3561ull, 0x41d772ea071259caull}, + {0xc035232f2cb69934ull, 0xc1c68c16c49344beull}, + }; + for (const auto& [input, expected] : cases) { + const std::uint64_t result = std::bit_cast(sinh_nid_postfix(std::bit_cast(input))); + if (result != expected) { + std::fprintf(stderr, "Guest sinh failed for %016llx: got %016llx, expected %016llx\n", static_cast(input), static_cast(result), static_cast(expected)); + std::abort(); + } + } +} + static void CheckRoundingNaNConsoleBits() { constexpr std::uint64_t cases[][2] = { {0x7ff0000000000001ull, 0x7ff8000000000001ull}, {0xfff4000000000123ull, 0xfffc000000000123ull}, {0x7ff8000000000456ull, 0x7ff8000000000456ull}, @@ -1916,6 +1946,7 @@ int main() { CheckLog1pConsoleBits(); CheckNextafterConsoleBits(); CheckTanhConsoleBits(); + CheckSinhMsunBits(); CheckRoundingNaNConsoleBits(); CheckNextafterEnvironment(nextafterf_nid_postfix, 0x80000000u, 0x3f800000u, 0x00800000u, 0x7f7fffffu, 0x7f800000u); CheckNextafterEnvironment(nextafter_nid_postfix, std::uint64_t{0x8000000000000000ull}, std::uint64_t{0x3ff0000000000000ull}, std::uint64_t{0x0010000000000000ull}, std::uint64_t{0x7fefffffffffffffull}, std::uint64_t{0x7ff0000000000000ull}); From 113d62139960df3a7b66d99a0269ebca1261a5cf Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:04:09 +0000 Subject: [PATCH 154/702] test(agc): give the performance overlay test 120 s in an ASan build --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 29669383a9..9d94af1bb1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1996,6 +1996,9 @@ if(BUILD_TESTING) add_test(NAME agc_performance_overlay_device_selection_cpu COMMAND agc_performance_overlay_tests device-selection-cpu) set_tests_properties(agc_performance_overlay agc_performance_overlay_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) set_tests_properties(agc_performance_overlay_device_selection_cpu PROPERTIES TIMEOUT 30) + if(ANYPS5_ENABLE_ASAN) + set_tests_properties(agc_performance_overlay PROPERTIES TIMEOUT 120) + endif() if(WIN32) set_tests_properties(agc_performance_overlay_statistics agc_performance_overlay agc_performance_overlay_device_selection agc_performance_overlay_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") From 5a9631df3b22f9ecc3632e9f4ab2d620365ee74e Mon Sep 17 00:00:00 2001 From: javierUDP Date: Mon, 26 Oct 2026 16:49:15 -0300 Subject: [PATCH 155/702] docs(relinker): record SHA, AVX-512 and XSAVE under Rosetta on an M5 The entry for the M5 running macOS 26.4 did not say what Rosetta does with SHA or AVX-512, and the XSAVE entry was measured only on macOS 27.0.1. On that host sha1rnds4 and a 256-bit-wide vpaddd on zmm raise an illegal instruction alongside RDSEED, CLWB, RDPID, ADCX and ADOX, while XSAVE writes an XSTATE_BV of 0x7 and XGETBV returns the same x87, SSE and AVX mask although CPUID leaf 1 still reports neither XSAVE nor OSXSAVE. The BMI1 and BMI2 results were compared with the Intel SDM definitions over 200,000 random inputs each for PDEP, PEXT, BEXTR, BZHI, LZCNT and TZCNT, so the entry can say they compute the documented result rather than only that they avoid a fault. --- docs/dev/debt/relinker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index f2d970f219..85029211fa 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -37,10 +37,10 @@ - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - In macOS output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. -- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. 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. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. +- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX, ADOX, SHA and AVX-512F raise an illegal instruction. On that host the BMI1 and BMI2 results match the Intel SDM definitions over 200,000 random inputs each for PDEP, PEXT, BEXTR, BZHI, LZCNT and TZCNT, so they compute the documented result and do not only avoid a fault. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. 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. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-rosetta` output faults on an M2 running macOS 14.8.9 when it reaches AVX, AVX2, FMA, F16C, RDRAND or a BMI1/BMI2 instruction other than ANDN, TZCNT and LZCNT: Rosetta on that host raises SIGILL for them and for RDSEED, RDPID, CLWB, ADCX and ADOX ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Only TZCNT, LZCNT, POPCNT, MOVBE, CLFLUSHOPT, PREFETCHW and RDTSCP execute, and RDTSCP sets ECX to 0xAAAAAAAB. CPUID leaf 1 reports POPCNT and none of MOVBE, AVX, FMA, F16C or RDRAND, leaf 7 reads zero, and leaf 0x80000001 reports RDTSCP but neither LZCNT nor PREFETCHW. An M2 Max on macOS 27.2 reports the same CPUID but executes everything except RDSEED, RDPID, CLWB, ADCX and ADOX, so the faults follow the macOS version, not the chip. - `--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). -- XSAVEC raises an illegal instruction under Rosetta and is not converted, because it writes the compacted format. XSAVEOPT, which also faults there, is rewritten to XSAVE in place by the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) under `--to-rosetta` (`0F AE /6` becomes `/4`; prefixes and operand are kept). The two differ only in the init and modified optimizations, which let XSAVEOPT skip writing unchanged or initial state ([Intel SDM volume 1](https://cdrdv2-public.intel.com/812382/253665-sdm-vol-1.pdf), chapter 13), so XRSTOR restores the same state from either area. Measured on one M5 with macOS 27.0.1: XSAVE and XRSTOR handle x87, SSE and AVX state in the standard format, although CPUID leaf 1 reports neither XSAVE nor OSXSAVE and leaf 0xD sub-leaf 1 reports neither XSAVEOPT nor XSAVEC. The converter tests' x86-64 build executes the replacement XSAVE under Rosetta on that host. +- XSAVEC raises an illegal instruction under Rosetta and is not converted, because it writes the compacted format. XSAVEOPT, which also faults there, is rewritten to XSAVE in place by the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) under `--to-rosetta` (`0F AE /6` becomes `/4`; prefixes and operand are kept). The two differ only in the init and modified optimizations, which let XSAVEOPT skip writing unchanged or initial state ([Intel SDM volume 1](https://cdrdv2-public.intel.com/812382/253665-sdm-vol-1.pdf), chapter 13), so XRSTOR restores the same state from either area. Measured on one M5 with macOS 27.0.1: XSAVE and XRSTOR handle x87, SSE and AVX state in the standard format, although CPUID leaf 1 reports neither XSAVE nor OSXSAVE and leaf 0xD sub-leaf 1 reports neither XSAVEOPT nor XSAVEC. XSAVE also executes on an M5 with macOS 26.4, where it writes an XSTATE_BV of 0x7 and XGETBV returns the same x87, SSE and AVX mask although leaf 1 still reports neither XSAVE nor OSXSAVE; XRSTOR was not exercised there. The converter tests' x86-64 build executes the replacement XSAVE under Rosetta on that host. - `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp), [macOS entry](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)) leaves every import that is not a PLT slot or a function GOT slot untraced, and the same for a function reached through any other relocation, `sceKernelDlsym`, a weak import, or a module loaded at run time. On Linux the entry stub makes the page of every traced slot readable, writable and executable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable, and data pages become executable, while tracing is on. On macOS the stub writes the thunk into the bound slot and does not change its protection, because that slot stays in a writable segment - [ANDN lowering](../../../core/relinker/codegen/src/x86/AndnLowering.cpp) uses scalar NOT and AND for 32- and 64-bit register and memory operands under `--to-rosetta`, following the ANDN entry in [Intel SDM volume 2A](https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf). SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws; other BMI instructions are not lowered. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified. From f2abc7e4f6217172ef2caa82dea1e309a2050e41 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 26 Oct 2026 15:31:16 -0400 Subject: [PATCH 156/702] test(shader): cover folded cast and NaN reuse boundaries --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 956897cb51..84b682ac38 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -474,7 +474,107 @@ void testInternedSpirvReleasesUnusedWords() { check(std::ranges::equal(again.Words(), words) && released.Lock().empty(), "SPIR-V interned again after its release changed its words"); } +std::vector foldedBitcasts(bool separateBlocks, bool differentInput, bool differentType, std::uint32_t nanOpcode, std::uint32_t decorationMask = 0u) { + SpirvModule module; + module.EmitCapability(spv::CapabilityShader); + module.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + const auto voidType = module.Type(spv::OpTypeVoid); + const auto boolType = module.Type(spv::OpTypeBool); + const auto wordType = module.Type(spv::OpTypeInt, 32u, 0u); + const auto floatType = module.Type(spv::OpTypeFloat, 32u); + const auto otherType = differentType ? module.Type(spv::OpTypeInt, 32u, 1u) : floatType; + const auto vectorType = module.Type(spv::OpTypeVector, wordType, 3u); + const auto input = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassInput, vectorType), spv::StorageClassInput); + module.AddAnnotation(spv::OpDecorate, input, spv::DecorationBuiltIn, spv::BuiltInGlobalInvocationId); + const auto firstOutputType = nanOpcode != 0u ? boolType : floatType; + const auto secondOutputType = nanOpcode != 0u ? boolType : otherType; + const std::array outputs{ + module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassPrivate, firstOutputType), spv::StorageClassPrivate), + module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassPrivate, secondOutputType), spv::StorageClassPrivate)}; + const auto mainId = module.AllocateId(); + const auto functionType = module.Type(spv::OpTypeFunction, voidType); + const auto zero = module.Constant(spv::OpConstant, wordType, 0u); + const auto trueValue = module.Constant(spv::OpConstantTrue, boolType); + module.EmitEntryPoint(spv::ExecutionModelGLCompute, mainId, "main", {input}); + module.AddExecutionMode(mainId, spv::ExecutionModeLocalSize, 1u, 1u, 1u); + module.AddFunction(spv::OpFunction, voidType, mainId, spv::FunctionControlMaskNone, functionType); + module.AddFunction(spv::OpLabel, module.AllocateId()); + const auto loaded = module.AllocateId(); + const auto first = module.AllocateId(); + const auto second = module.AllocateId(); + const auto alias = module.AllocateId(); + module.AddFunction(spv::OpLoad, vectorType, loaded, input); + module.AddFunction(spv::OpCompositeExtract, wordType, first, loaded, 0u); + module.AddFunction(spv::OpCompositeExtract, wordType, second, loaded, 1u); + module.AddFunction(spv::OpSelect, wordType, alias, trueValue, first, second); + const auto emit = [&](bool other) { + const auto cast = module.AllocateId(); + module.AddFunction(spv::OpBitcast, other ? otherType : floatType, cast, other ? (differentInput ? second : alias) : first); + if ((decorationMask & (other ? 2u : 1u)) != 0u) module.AddAnnotation(spv::OpDecorate, cast, spv::DecorationRelaxedPrecision); + if ((decorationMask & (other ? 8u : 4u)) != 0u) { + const auto group = module.AllocateId(); + module.AddAnnotation(spv::OpDecorationGroup, group); + module.AddAnnotation(spv::OpDecorate, group, spv::DecorationRelaxedPrecision); + module.AddAnnotation(spv::OpGroupDecorate, group, cast); + } + auto result = cast; + if (nanOpcode != 0u) { + result = module.AllocateId(); + if (nanOpcode == spv::OpIsNan) module.AddFunction(nanOpcode, boolType, result, cast); + else module.AddFunction(nanOpcode, boolType, result, cast, cast); + } + module.AddFunction(spv::OpStore, outputs[other ? 1u : 0u], result); + }; + if (separateBlocks) { + const auto condition = module.AllocateId(); + const auto left = module.AllocateId(); + const auto right = module.AllocateId(); + const auto merge = module.AllocateId(); + module.AddFunction(spv::OpINotEqual, boolType, condition, first, zero); + module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + module.AddFunction(spv::OpBranchConditional, condition, left, right); + module.AddFunction(spv::OpLabel, left); + emit(false); + module.AddFunction(spv::OpBranch, merge); + module.AddFunction(spv::OpLabel, right); + emit(true); + module.AddFunction(spv::OpBranch, merge); + module.AddFunction(spv::OpLabel, merge); + } else { + emit(false); + emit(true); + } + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + return SpecializeSpirv(module.Finalize()); +} + +void testFoldedBitcastsAndNanTests() { + for (const auto nanOpcode : std::array{0u, spv::OpIsNan, spv::OpFUnordNotEqual}) { + const auto same = opcodeCounts(foldedBitcasts(false, false, false, nanOpcode)); + const auto other = opcodeCounts(foldedBitcasts(false, true, false, nanOpcode)); + const auto branches = opcodeCounts(foldedBitcasts(true, false, false, nanOpcode)); + const auto count = [](const auto& counts, std::uint32_t op) { + const auto found = counts.find(op); + return found == counts.end() ? 0u : found->second; + }; + check(count(same, spv::OpBitcast) == 1u, "folding a select left repeated bitcasts of one value in a block"); + check(count(other, spv::OpBitcast) == 2u, "folding merged bitcasts of different input values"); + check(count(branches, spv::OpBitcast) == 2u, "folding reused a bitcast from a sibling branch"); + if (nanOpcode != 0u) { + check(count(same, nanOpcode) == 1u, "folding left repeated NaN tests of one value in a block"); + check(count(other, nanOpcode) == 2u, "folding merged NaN tests of different input values"); + check(count(branches, nanOpcode) == 2u, "folding reused a NaN test from a sibling branch"); + } + } + check(opcodeCounts(foldedBitcasts(false, false, true, 0u))[spv::OpBitcast] == 2u, "folding merged bitcasts to different result types"); + for (const auto decorations : {1u, 2u, 3u, 4u, 8u}) { + check(opcodeCounts(foldedBitcasts(false, false, false, 0u, decorations))[spv::OpBitcast] == 2u, "folding merged a decorated bitcast with another value"); + } +} + int main() { + testFoldedBitcastsAndNanTests(); testFoldingLeavesNoDeadBlocksOrConstants(); testIdsBeyondTheBoundAreRejected(); testStructExtractsAreNotShuffled(); From 09c36584fc458d0d1dc60f27f155947e06cfb27e Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 26 Oct 2026 15:39:41 -0400 Subject: [PATCH 157/702] fix(shader): reuse folded bitcasts and NaN tests within blocks --- .../SpirvBackend/src/SpirvSpecialization.cpp | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index a47da0f9dd..5268613c5c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -3,6 +3,7 @@ #undef SPV_ENABLE_UTILITY_CODE #include "SpirvBackend/SpirvSpecialization.hpp" #include +#include #include #include #include @@ -579,6 +580,7 @@ public: resultTypes.Reset(bound); vectorTypes.Reset(bound); removed.Reset(bound); + decoratedResults.Reset(bound); aliases.Reset(bound); composites.Reset(bound); privateTables.Reset(bound); @@ -592,6 +594,8 @@ public: bool hasType = false; spv::HasResultAndType(op, &hasResult, &hasType); if (hasResult && hasType) resultTypes.emplace(instruction.at(2), instruction.at(1)); + if (op == spv::OpDecorate || op == spv::OpDecorateId || op == spv::OpDecorateString) decoratedResults.insert(instruction.at(1)); + if (op == spv::OpGroupDecorate) for (std::size_t index = 2; index < instruction.size(); ++index) decoratedResults.insert(instruction[index]); if (op == spv::OpTypeBool) types.emplace(instruction.at(1), ScalarType{1u, true}); if (op == spv::OpTypeInt) types.emplace(instruction.at(1), ScalarType{instruction.at(2), false}); if (op == spv::OpTypeVector) vectorTypes.insert(instruction.at(1)); @@ -720,11 +724,17 @@ private: extracts.Reset(bound); IdTable bitcasts; bitcasts.Reset(bound); + std::map, std::uint32_t> blockBitcasts; + std::map, std::uint32_t> blockNanTests; IdTable> constPointers; constPointers.Reset(bound); for (auto& instruction : instructions) { if (instruction.empty()) continue; auto op = Opcode(instruction); + if (op == spv::OpFunction || op == spv::OpLabel) { + blockBitcasts.clear(); + blockNanTests.clear(); + } if (op == spv::OpFunction) function = true; if (op == spv::OpFunctionEnd) function = false; if (!function) continue; @@ -771,7 +781,23 @@ private: instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], source}); changed = true; } else { - bitcasts.emplace(instruction[2], instruction[3]); + const auto [found, inserted] = decoratedResults.contains(instruction[2]) + ? std::pair{blockBitcasts.end(), true} + : blockBitcasts.try_emplace(std::pair{instruction[1], instruction[3]}, instruction[2]); + if (inserted) { + bitcasts.emplace(instruction[2], instruction[3]); + } else { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], found->second}); + changed = true; + } + } + } + const bool nanTest = (op == spv::OpIsNan && instruction.size() == 4u) || (op == spv::OpFUnordNotEqual && instruction.size() == 5u && instruction[3] == instruction[4]); + if (nanTest && !decoratedResults.contains(instruction[2])) { + const auto [found, inserted] = blockNanTests.try_emplace(std::array{op, instruction[1], instruction[3]}, instruction[2]); + if (!inserted) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], found->second}); + changed = true; } } op = Opcode(instruction); @@ -1147,6 +1173,7 @@ private: IdTable resultTypes; IdSet vectorTypes; IdSet removed; + IdSet decoratedResults; IdTable aliases; IdTable> composites; IdTable privateTables; From e1979dffde7d735b204e94510c3096e327afb211 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:04:47 +0100 Subject: [PATCH 158/702] test(libc): cover sinh from 22 up to the overflow threshold Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestMath.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6eb8baf7a8..e9b5b2a723 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -681,6 +681,33 @@ static void CheckSinhMsunBits() { } } +static bool SinhWithinUlps(std::uint64_t result, std::uint64_t expected, std::uint64_t ulps) { + return ((result ^ expected) >> 63) == 0 && (result > expected ? result - expected : expected - result) <= ulps; +} + +static void CheckSinhLargeArguments() { + constexpr std::uint64_t cases[][2] = { + {0x4036000000000000ull, 0x41dab5adb9c43600ull}, {0x4036000000000001ull, 0x41dab5adb9c4361aull}, + {0x4059000000000000ull, 0x48e3494a9b171bf5ull}, {0x4062c00000000000ull, 0x4d652cac29822593ull}, + {0x40862e41ffffffffull, 0x7fdffc045692fc9eull}, {0x40862e4200000000ull, 0x7fdffc045693009eull}, + {0x40862e4200000001ull, 0x7fdffc045693049eull}, {0x4086330000000000ull, 0x7fecee0e3df7d1f3ull}, + {0x408633cdffffffffull, 0x7feffdc12c49df1cull}, {0x408633ce00000000ull, 0x7feffdc12c49e31cull}, + {0x408633ce8fb9e87dull, 0x7fefffffffbffd3cull}, {0x408633ce8fba087dull, 0x7ff0000000000000ull}, + {0x408633ceffffffffull, 0x7ff0000000000000ull}, {0x408633cf00000000ull, 0x7ff0000000000000ull}, + {0x7fefffffffffffffull, 0x7ff0000000000000ull}, + }; + for (const auto& [input, expected] : cases) { + for (const std::uint64_t sign : {std::uint64_t{0}, std::uint64_t{1} << 63}) { + const std::uint64_t result = std::bit_cast(sinh_nid_postfix(std::bit_cast(input | sign))); + const bool overflow = expected == 0x7ff0000000000000ull; + if (overflow ? result != (expected | sign) : !SinhWithinUlps(result, expected | sign, 4)) { + std::fprintf(stderr, "Guest sinh failed for %016llx: got %016llx, expected %016llx\n", static_cast(input | sign), static_cast(result), static_cast(expected | sign)); + std::abort(); + } + } + } +} + static void CheckRoundingNaNConsoleBits() { constexpr std::uint64_t cases[][2] = { {0x7ff0000000000001ull, 0x7ff8000000000001ull}, {0xfff4000000000123ull, 0xfffc000000000123ull}, {0x7ff8000000000456ull, 0x7ff8000000000456ull}, @@ -1947,6 +1974,7 @@ int main() { CheckNextafterConsoleBits(); CheckTanhConsoleBits(); CheckSinhMsunBits(); + CheckSinhLargeArguments(); CheckRoundingNaNConsoleBits(); CheckNextafterEnvironment(nextafterf_nid_postfix, 0x80000000u, 0x3f800000u, 0x00800000u, 0x7f7fffffu, 0x7f800000u); CheckNextafterEnvironment(nextafter_nid_postfix, std::uint64_t{0x8000000000000000ull}, std::uint64_t{0x3ff0000000000000ull}, std::uint64_t{0x0010000000000000ull}, std::uint64_t{0x7fefffffffffffffull}, std::uint64_t{0x7ff0000000000000ull}); From c78810a2debae9674bf638da73466f327931d117 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:14:17 +0100 Subject: [PATCH 159/702] test(libc): cover the first sinh argument past ln(DBL_MAX) Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestMath.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index e9b5b2a723..435b7a9b11 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -690,7 +690,8 @@ static void CheckSinhLargeArguments() { {0x4036000000000000ull, 0x41dab5adb9c43600ull}, {0x4036000000000001ull, 0x41dab5adb9c4361aull}, {0x4059000000000000ull, 0x48e3494a9b171bf5ull}, {0x4062c00000000000ull, 0x4d652cac29822593ull}, {0x40862e41ffffffffull, 0x7fdffc045692fc9eull}, {0x40862e4200000000ull, 0x7fdffc045693009eull}, - {0x40862e4200000001ull, 0x7fdffc045693049eull}, {0x4086330000000000ull, 0x7fecee0e3df7d1f3ull}, + {0x40862e4200000001ull, 0x7fdffc045693049eull}, {0x40862e42ffffffffull, 0x7fe000020b8c410bull}, + {0x4086330000000000ull, 0x7fecee0e3df7d1f3ull}, {0x408633cdffffffffull, 0x7feffdc12c49df1cull}, {0x408633ce00000000ull, 0x7feffdc12c49e31cull}, {0x408633ce8fb9e87dull, 0x7fefffffffbffd3cull}, {0x408633ce8fba087dull, 0x7ff0000000000000ull}, {0x408633ceffffffffull, 0x7ff0000000000000ull}, {0x408633cf00000000ull, 0x7ff0000000000000ull}, From 6e78df998063d9ea3740019e0d40303dec1184fe Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 20:43:41 +0000 Subject: [PATCH 160/702] test(libSceNpUniversalDataSystem): cover initialization, the memory and storage stats and the event size and string queries Co-Authored-By: Claude Opus 5.5 --- .../CMakeLists.txt | 9 +++ .../GuestNpUniversalDataSystemQueries.cpp | 69 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 core/libs/tests/GuestNpUniversalDataSystemQueries.cpp diff --git a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt index 62016d6a55..fe55c42c8b 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt +++ b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt @@ -64,4 +64,13 @@ if(BUILD_TESTING) set_tests_properties(guest_np_trophy_stats PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_np_uds_queries_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpUniversalDataSystemQueries.cpp) + target_include_directories(guest_np_uds_queries_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_uds_queries_tests PRIVATE libSceNpUniversalDataSystem libc) + configure_windows_unwind(guest_np_uds_queries_tests) + add_test(NAME guest_np_uds_queries COMMAND guest_np_uds_queries_tests) + if(WIN32) + set_tests_properties(guest_np_uds_queries PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/tests/GuestNpUniversalDataSystemQueries.cpp b/core/libs/tests/GuestNpUniversalDataSystemQueries.cpp new file mode 100644 index 0000000000..222d6f21a9 --- /dev/null +++ b/core/libs/tests/GuestNpUniversalDataSystemQueries.cpp @@ -0,0 +1,69 @@ +#include "prx/libSceNpUniversalDataSystem/include/Api.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +static void Require(bool value, int line) { + if (value) return; + std::fprintf(stderr, "UDS query check failed at line %d\n", line); + std::abort(); +} +#define CHECK(value) Require((value), __LINE__) + +template +static bool RejectsArgument(Call&& call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +template +static bool Zeroed(const Stat& stat) { + const auto* bytes = reinterpret_cast(&stat); + for (std::size_t index = 0; index < sizeof(stat); ++index) { + if (bytes[index] != 0) return false; + } + return true; +} + +int main() { + CHECK(RejectsArgument([] { sceNpUniversalDataSystemInitialize(nullptr); })); + const NpUniversalDataSystemInitParam init{sizeof(NpUniversalDataSystemInitParam), 0x10000}; + CHECK(sceNpUniversalDataSystemInitialize(&init) == 0); + + NpUniversalDataSystemMemoryStat memory; + std::memset(&memory, 0x5a, sizeof(memory)); + CHECK(RejectsArgument([] { sceNpUniversalDataSystemGetMemoryStat(nullptr); })); + CHECK(sceNpUniversalDataSystemGetMemoryStat(&memory) == 0 && Zeroed(memory)); + NpUniversalDataSystemStorageStat storage; + std::memset(&storage, 0x5a, sizeof(storage)); + CHECK(RejectsArgument([] { sceNpUniversalDataSystemGetStorageStat(1, nullptr); })); + CHECK(sceNpUniversalDataSystemGetStorageStat(1, &storage) == 0 && Zeroed(storage)); + + NpUniversalDataSystemEvent* event = nullptr; + CHECK(sceNpUniversalDataSystemCreateEvent("QueryEvent", nullptr, &event, nullptr) == 0 && event != nullptr); + std::size_t size = 0; + CHECK(RejectsArgument([&] { sceNpUniversalDataSystemEventEstimateSize(nullptr, &size); })); + CHECK(RejectsArgument([&] { sceNpUniversalDataSystemEventEstimateSize(event, nullptr); })); + CHECK(sceNpUniversalDataSystemEventEstimateSize(event, &size) == 0 && size == 3); + + char text[8]; + std::memset(text, 'x', sizeof(text)); + std::size_t length = 0; + CHECK(RejectsArgument([&] { sceNpUniversalDataSystemEventToString(nullptr, text, sizeof(text), &length); })); + CHECK(text[0] == 'x' && length == 0); + CHECK(sceNpUniversalDataSystemEventToString(event, text, sizeof(text), &length) == 0 && length == 3 && std::strcmp(text, "{}") == 0); + length = 0; + CHECK(sceNpUniversalDataSystemEventToString(event, nullptr, 0, &length) == 0 && length == 3); + char tiny[2] = {'x', 'x'}; + CHECK(sceNpUniversalDataSystemEventToString(event, tiny, sizeof(tiny), nullptr) == 0 && tiny[0] == '{' && tiny[1] == '\0'); + CHECK(sceNpUniversalDataSystemDestroyEvent(event) == 0); + std::puts("UDS query tests passed"); + return 0; +} From 3e7492d6008bfd25936b922ecd892fde4543722b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 20:47:29 +0000 Subject: [PATCH 161/702] test(libSceUlt): cover the mutex and ulthread runtime opt param initializers Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestUltSync.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp index f8e54452d0..601110ab9c 100644 --- a/core/libs/tests/GuestUltSync.cpp +++ b/core/libs/tests/GuestUltSync.cpp @@ -63,6 +63,23 @@ void MutexTryLock() { CHECK(sceUltMutexTryLock(&mutex) == State); } +void MutexAndRuntimeOptParams() { + alignas(8) std::uint8_t misaligned[sizeof(UltMutexOptParam) + 8]; + CHECK(sceUltMutexOptParamInitialize(nullptr, 0) == Null); + CHECK(sceUltMutexOptParamInitialize(reinterpret_cast(misaligned + 4), 0) == -2139029502); + UltMutexOptParam mutexOptions; + std::memset(&mutexOptions, 0xa5, sizeof(mutexOptions)); + CHECK(sceUltMutexOptParamInitialize(&mutexOptions, 0x1234) == Ok); + CHECK(mutexOptions.header[0] == 0x6C696275 && mutexOptions.header[1] == 0x6C746D61 && mutexOptions.header[2] == 0x67696300); + CHECK(mutexOptions.buildVersion == 0x1234 && mutexOptions.attribute == 0); + for (const std::uint32_t word : mutexOptions.reserved) CHECK(word == 0); + UltUlthreadRuntimeOptParam runtimeOptions; + std::memset(&runtimeOptions, 0xa5, sizeof(runtimeOptions)); + CHECK(sceUltUlthreadRuntimeOptParamInitialize(nullptr, 0) == Null); + CHECK(sceUltUlthreadRuntimeOptParamInitialize(&runtimeOptions, 0) == Ok); + for (const std::uint8_t byte : runtimeOptions.bytes) CHECK(byte == 0); +} + void OptParams() { using Initializer = decltype(&sceUltSemaphoreOptParamInitialize); for (const Initializer initialize : {sceUltSemaphoreOptParamInitialize, sceUltQueueOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, @@ -677,7 +694,10 @@ int main(int argc, char** argv) { if (argc != 2) return 2; CHECK(sceUltInitialize() == Ok); if (std::strcmp(argv[1], "mutex-trylock") == 0) MutexTryLock(); - else if (std::strcmp(argv[1], "opt-params") == 0) OptParams(); + else if (std::strcmp(argv[1], "opt-params") == 0) { + OptParams(); + MutexAndRuntimeOptParams(); + } else if (std::strcmp(argv[1], "rwlock-validation") == 0) ReaderWriterValidation(); else if (std::strcmp(argv[1], "rwlock-readers") == 0) ReaderWriterReaders(); else if (std::strcmp(argv[1], "rwlock-writer") == 0) ReaderWriterWriter(); From 9d35ca87c946e12c36b1dd193323167f68b63cb2 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 20:47:55 +0000 Subject: [PATCH 162/702] test(libkernel): cover scePthreadGetprio after scePthreadSetprio on the calling thread Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestPthreadAttr.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index e15870361b..ca0695d96d 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -326,7 +326,23 @@ static void CheckEndedThreads() { Require(scePthreadAttrDestroy(&detachedAttr) == SCE_OK); } +static void CheckGetprioOfTheCallingThread() { + const Pthread self = scePthreadSelf(); + int original = 0; + Require(scePthreadGetprio(self, &original) == SCE_OK); + int priority = 0; + Require(scePthreadSetprio(self, UPDATED_PRIORITY) == SCE_OK); + Require(scePthreadGetprio(self, &priority) == SCE_OK && priority == UPDATED_PRIORITY); + Require(scePthreadSetprio(self, CREATION_PRIORITY) == SCE_OK); + Require(scePthreadGetprio(self, &priority) == SCE_OK && priority == CREATION_PRIORITY); + Require(scePthreadGetprio(self, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadGetprio(nullptr, &priority) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadSetprio(self, original) == SCE_OK); + Require(scePthreadGetprio(self, &priority) == SCE_OK && priority == original); +} + int main() { + CheckGetprioOfTheCallingThread(); CheckStackSizeLimit(); CheckSoloSched(); CheckSchedArguments(); From 008c5e8e37e1ea3aacf248bed394d7708deb4772 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 18:04:12 +0000 Subject: [PATCH 163/702] test(libkernel): cover sceKernelMemoryPoolGetBlockStats's checks and the block an expansion adds Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestMemory.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index f31f21accc..f8ae4d11ca 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -3307,6 +3307,23 @@ static void CheckMunmapOverUnmappedPages() { Require(sceKernelMunmap(reinterpret_cast(memory), 0) == SCE_KERNEL_ERROR_EINVAL); } +static void CheckMemoryPoolBlockStats() { + KernelMemoryPoolBlockStats stats{-1, -1, -1, -1}; + Require(sceKernelMemoryPoolGetBlockStats(nullptr, sizeof(stats)) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelMemoryPoolGetBlockStats(&stats, sizeof(stats) - 1) == SCE_KERNEL_ERROR_EINVAL); + Require(stats.available_flushed_blocks == -1 && stats.allocated_flushed_blocks == -1); + Require(sceKernelMemoryPoolGetBlockStats(&stats, sizeof(stats)) == 0); + Require(stats.available_cached_blocks == 0 && stats.allocated_cached_blocks == 0); + Require(stats.available_flushed_blocks == 0 && stats.allocated_flushed_blocks == 0); + constexpr std::size_t block = std::size_t{2} << 20; + std::int64_t phys = -1; + Require(sceKernelMemoryPoolExpand(0, static_cast(sceKernelGetDirectMemorySize()), block, block, &phys) == 0); + Require(phys >= 0 && phys % static_cast(block) == 0); + Require(sceKernelMemoryPoolGetBlockStats(&stats, sizeof(stats)) == 0); + Require(stats.available_flushed_blocks == 1 && stats.allocated_flushed_blocks == 0); + Require(sceKernelReleaseDirectMemory(phys, block) == 0); +} + int main(int argc, char** argv) { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "1000"); @@ -3323,6 +3340,7 @@ int main(int argc, char** argv) { CheckAvailableDirectMemoryIsLargestFreeBlock(); CheckVirtualQuerySplitFlexibleRanges(); CheckVirtualQueryForNonReadableGuestRange(); + CheckMemoryPoolBlockStats(); CheckReleaseFlexibleMemory(); CheckFlexibleMappingIgnoresFlag8000(); CheckSceKernelMmap(); From ae79efaeb899496931bf2a832b942ffee3a59d41 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 21:00:32 +0000 Subject: [PATCH 164/702] test(libkernel): cover scePthreadRwlockattrInit and scePthreadRwlockattrDestroy around a lock they initialize Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestPthreadRwlock.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 1f6783b2d5..bff001e376 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -412,7 +412,31 @@ static void CheckAttrType() { Require(Throws([&] { scePthreadRwlockattrGettype(&unset, &type); })); } +static void CheckSceAttrLifecycle() { + PthreadRwlockattr attr = nullptr; + Require(scePthreadRwlockattrInit(&attr) == SCE_OK && attr != nullptr); + PthreadRwlock lock = nullptr; + Require(scePthreadRwlockInit(&lock, &attr, "sce attr") == SCE_OK && lock != nullptr); + Require(pthread_rwlock_wrlock_nid_postfix(&lock) == 0); + Require(pthread_rwlock_tryrdlock_nid_postfix(&lock) == GUEST_EBUSY); + Require(pthread_rwlock_unlock_nid_postfix(&lock) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&lock) == 0); + Require(scePthreadRwlockattrDestroy(&attr) == SCE_OK && attr == nullptr); + const auto throws = [](auto&& call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; + }; + Require(throws([&] { scePthreadRwlockattrDestroy(&attr); })); + Require(throws([] { scePthreadRwlockattrDestroy(nullptr); })); + Require(throws([] { scePthreadRwlockattrInit(nullptr); })); +} + int main() { + CheckSceAttrLifecycle(); CheckAttrSettype(); CheckAttrProcessSharing(); CheckAttrType(); From 646920d12a24c91d338922c2c1d8599a76ab20c6 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 21:05:09 +0000 Subject: [PATCH 165/702] test(libSceUsbd): cover the bulk, iso and control transfer fillers and their accessors Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestUsbd.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index eb6cc9af44..0e8f5ecee6 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -241,8 +241,38 @@ void Device() { } +void CheckTransferFillers() { + auto* transfer = sceUsbdAllocTransfer(3); + Require(transfer != nullptr, "transfer allocation failed"); + std::uint8_t data[64] = {}; + int device = 0; + int user = 0; + sceUsbdFillBulkTransfer(transfer, &device, 0x02, data, 64, &MarkDone, &user, 500); + Require(transfer->deviceHandle == &device && transfer->endpoint == 0x02 && transfer->type == 2 && transfer->timeout == 500, "bulk transfer target not filled"); + Require(transfer->buffer == data && transfer->length == 64 && transfer->userData == &user && transfer->callback == &MarkDone, "bulk transfer buffer or callback not filled"); + sceUsbdFillIsoTransfer(transfer, &device, 0x83, data, 48, 3, &MarkDone, &user, 0); + Require(transfer->endpoint == 0x83 && transfer->type == 1 && transfer->length == 48 && transfer->numIsoPackets == 3, "iso transfer not filled"); + sceUsbdSetIsoPacketLengths(transfer, 16); + for (int packet = 0; packet < 3; ++packet) Require(transfer->isoPacketDescriptors[packet].length == 16, "iso packet length not set"); + Require(sceUsbdGetIsoPacketBuffer(transfer, 0) == data && sceUsbdGetIsoPacketBuffer(transfer, 1) == data + 16 && sceUsbdGetIsoPacketBuffer(transfer, 2) == data + 32, "iso packet buffers do not follow the packet lengths"); + Require(sceUsbdGetIsoPacketBuffer(transfer, 3) == nullptr, "a packet past the last one has a buffer"); + std::uint8_t control[12] = {}; + sceUsbdFillControlSetup(control, 0x80, 6, 0x0100, 0x0409, 4); + Require(control[0] == 0x80 && control[1] == 6 && control[2] == 0x00 && control[3] == 0x01 && control[4] == 0x09 && control[5] == 0x04 && control[6] == 4 && control[7] == 0, "control setup not packed little-endian"); + sceUsbdFillControlTransfer(transfer, &device, control, &MarkDone, &user, 100); + Require(transfer->type == 0 && transfer->endpoint == 0 && transfer->length == 12 && transfer->buffer == control && transfer->timeout == 100, "control transfer not filled from its setup"); + Require(sceUsbdControlTransferGetSetup(transfer) == control && sceUsbdControlTransferGetData(transfer) == control + 8, "control setup or data pointer wrong"); + sceUsbdFillControlTransfer(transfer, &device, nullptr, &MarkDone, &user, 100); + Require(transfer->length == 0 && sceUsbdControlTransferGetData(transfer) == nullptr, "a control transfer without a buffer has data"); + Require(Throws([] { sceUsbdFillBulkTransfer(nullptr, nullptr, 0, nullptr, 0, nullptr, nullptr, 0); }), "filling a null bulk transfer did not throw"); + Require(Throws([] { sceUsbdFillControlSetup(nullptr, 0, 0, 0, 0, 0); }), "filling a null control setup did not throw"); + Require(Throws([] { sceUsbdGetIsoPacketBuffer(nullptr, 0); }), "the packet buffer of a null transfer did not throw"); + sceUsbdFreeTransfer(transfer); +} + int main(int argc, char** argv) { Require(sceUsbdInit() == 0, "initialization failed"); + CheckTransferFillers(); void** list = nullptr; const std::int64_t count = sceUsbdGetDeviceList(&list); Require(count >= 0 && list != nullptr && list[count] == nullptr, "device list is not a null-terminated array of the reported length"); From 8ca226f420f9b5119a0555abcdbd79eb5e0dbacd Mon Sep 17 00:00:00 2001 From: Alvaro Date: Sat, 24 Oct 2026 02:47:14 +0200 Subject: [PATCH 166/702] feat(libSceLibreSsl): implement X509_STORE_CTX_get_error --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 13 +++- core/libs/prx/libSceLibreSsl/Export.cpp | 5 -- .../libSceLibreSsl/src/X509StoreContext.cpp | 53 ++++++++++++++++ .../tests/GuestLibreSslX509StoreContext.cpp | 63 +++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 3 +- 5 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp create mode 100644 core/libs/tests/GuestLibreSslX509StoreContext.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 3621baad39..bd29f64566 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rsa.cpp) +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rsa.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/X509StoreContext.cpp) target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) @@ -121,6 +121,17 @@ if(BUILD_TESTING) set_tests_properties(guest_libressl_rsa PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_libressl_x509_store_context_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslX509StoreContext.cpp) + target_include_directories(guest_libressl_x509_store_context_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_x509_store_context_tests PRIVATE libSceLibreSsl) + configure_windows_unwind(guest_libressl_x509_store_context_tests) + add_test(NAME guest_libressl_x509_store_context COMMAND guest_libressl_x509_store_context_tests) + set_tests_properties(guest_libressl_x509_store_context PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_x509_store_context PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Cipher.cpp) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 00ace935b0..db3781767f 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1493,11 +1493,6 @@ int APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(void) { return 0; } -int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp new file mode 100644 index 0000000000..ff9b921fb1 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp @@ -0,0 +1,53 @@ +#include +#include +#include +#include "prx/libc/include/General.hpp" + +struct LibreSslX509StoreContext { + void* store; + int currentMethod; + void* cert; + void* untrusted; + void* crls; + void* param; + void* otherContext; + void* callbacks[12]; + int valid; + int lastUntrusted; + void* chain; + void* tree; + int explicitPolicy; + int errorDepth; + int error; + void* currentCert; + void* currentIssuer; + void* currentCrl; + int currentCrlScore; + unsigned int currentReasons; + LibreSslX509StoreContext* parent; + void* exData; +}; + +static_assert(sizeof(LibreSslX509StoreContext) == 240); +static_assert(offsetof(LibreSslX509StoreContext, chain) == 160); +static_assert(offsetof(LibreSslX509StoreContext, errorDepth) == 180); +static_assert(offsetof(LibreSslX509StoreContext, error) == 184); +static_assert(offsetof(LibreSslX509StoreContext, currentCert) == 192); +static_assert(offsetof(LibreSslX509StoreContext, exData) == 232); + +namespace { + +LibreSslX509StoreContext* Context(LibreSslX509StoreContext* context, const char* function) { + if (!context) throw std::invalid_argument(std::string(function) + ": null context"); + return context; +} + +} + +extern "C" { + +int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(LibreSslX509StoreContext* context) { + return Context(context, __func__)->error; +} + +} diff --git a/core/libs/tests/GuestLibreSslX509StoreContext.cpp b/core/libs/tests/GuestLibreSslX509StoreContext.cpp new file mode 100644 index 0000000000..ac91319d24 --- /dev/null +++ b/core/libs/tests/GuestLibreSslX509StoreContext.cpp @@ -0,0 +1,63 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void*); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL X509_STORE_CTX check failed at line %u\n", location.line()); + std::abort(); +} + +template static void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::exception&) { return; } + Require(false, location); +} + +constexpr std::size_t ContextSize = 240; +constexpr std::size_t ErrorOffset = 184; + +struct alignas(8) TestContext { + unsigned char bytes[ContextSize]; + + TestContext() { std::memset(bytes, 0xAA, sizeof(bytes)); } + + template void Write(std::size_t offset, TValue value) { std::memcpy(bytes + offset, &value, sizeof(value)); } + + template TValue Read(std::size_t offset) const { + TValue value; + std::memcpy(&value, bytes + offset, sizeof(value)); + return value; + } +}; + +static bool OnlyChanged(const TestContext& context, std::size_t offset, std::size_t size) { + for (std::size_t index = 0; index < ContextSize; ++index) + if ((index < offset || index >= offset + size) && context.bytes[index] != 0xAA) return false; + return true; +} + +static void TestGetError() { + TestContext context; + context.Write(ErrorOffset, 0); + Require(X509_STORE_CTX_get_error_nid_postfix(&context) == 0); + context.Write(ErrorOffset, 20); + Require(X509_STORE_CTX_get_error_nid_postfix(&context) == 20); + context.Write(ErrorOffset, -7); + Require(X509_STORE_CTX_get_error_nid_postfix(&context) == -7); + Require(OnlyChanged(context, ErrorOffset, 4)); + RequireThrows([] { X509_STORE_CTX_get_error_nid_postfix(nullptr); }); +} + +int main() { + TestGetError(); + std::puts("libSceLibreSsl X509_STORE_CTX tests passed"); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 723c8f3d9d..c82dbf31dd 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 319 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 318 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -15,6 +15,7 @@ - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. - [sk_num, sk_value, sk_pop, sk_pop_free](../../../core/libs/prx/libSceLibreSsl/src/Stack.cpp) (libSceLibreSsl) - the 32-byte `_STACK` layout (`num`, `data`, `sorted`, `num_alloc`, `comp`) and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/stack) `stack.h` and `stack.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. `sk_pop_free` releases the element array and the stack with the guest `free`, as the module would through libc. No export of this module creates a stack yet, so only stacks the caller builds reach them. A null element array that LibreSSL would read through, and a null free function with a non-null element, throw where LibreSSL faults - [RSA_flags, RSA_size](../../../core/libs/prx/libSceLibreSsl/src/Rsa.cpp) (libSceLibreSsl) - nothing is checked on the console. Layouts and results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto) `rsa.h`, `rsa_crpt.c` and `bn_lib.c`, the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export of this module creates a key yet, so only keys the caller builds reach them. `RSA_size` counts the modulus bytes from its top word, as `BN_num_bytes`, so a zero top word is not skipped. A null method, a null key or modulus, and a positive word count with null words, which LibreSSL dereferences, throw, as do a negative word count and a bit count that overflows `int` +- [X509_STORE_CTX_get_error](../../../core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp) (libSceLibreSsl) - the 240-byte `X509_STORE_CTX` layout and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_vfy.h` and `x509_vfy.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. No export of this module creates or fills a context yet, so only contexts the caller builds reach them. A null context, which LibreSSL dereferences, throws ### Functional From da1fa540c1342a02316cda5e4dee7a7bc51730e6 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Sat, 24 Oct 2026 02:47:24 +0200 Subject: [PATCH 167/702] feat(libSceLibreSsl): implement X509_STORE_CTX_set_error --- core/libs/prx/libSceLibreSsl/Export.cpp | 5 ----- .../prx/libSceLibreSsl/src/X509StoreContext.cpp | 4 ++++ core/libs/tests/GuestLibreSslX509StoreContext.cpp | 15 +++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 4 ++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index db3781767f..634199c2eb 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1503,11 +1503,6 @@ int APS5_VABI X509_STORE_CTX_get_ex_data_nid_postfix(void) { return 0; } -int APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI X509_STORE_add_cert_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp index ff9b921fb1..e0160983ee 100644 --- a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp +++ b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp @@ -50,4 +50,8 @@ int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(LibreSslX509StoreContext* con return Context(context, __func__)->error; } +void APS5_VABI X509_STORE_CTX_set_error_nid_postfix(LibreSslX509StoreContext* context, int error) { + Context(context, __func__)->error = error; +} + } diff --git a/core/libs/tests/GuestLibreSslX509StoreContext.cpp b/core/libs/tests/GuestLibreSslX509StoreContext.cpp index ac91319d24..c1cef4232c 100644 --- a/core/libs/tests/GuestLibreSslX509StoreContext.cpp +++ b/core/libs/tests/GuestLibreSslX509StoreContext.cpp @@ -9,6 +9,7 @@ extern "C" { int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void*); +void APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void*, int); } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -57,7 +58,21 @@ static void TestGetError() { RequireThrows([] { X509_STORE_CTX_get_error_nid_postfix(nullptr); }); } +static void TestSetError() { + TestContext context; + X509_STORE_CTX_set_error_nid_postfix(&context, 10); + Require(context.Read(ErrorOffset) == 10); + Require(X509_STORE_CTX_get_error_nid_postfix(&context) == 10); + X509_STORE_CTX_set_error_nid_postfix(&context, -1); + Require(context.Read(ErrorOffset) == -1); + X509_STORE_CTX_set_error_nid_postfix(&context, 0); + Require(context.Read(ErrorOffset) == 0); + Require(OnlyChanged(context, ErrorOffset, 4)); + RequireThrows([] { X509_STORE_CTX_set_error_nid_postfix(nullptr, 1); }); +} + int main() { TestGetError(); + TestSetError(); std::puts("libSceLibreSsl X509_STORE_CTX tests passed"); } diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index c82dbf31dd..5022af3091 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 318 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 317 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -15,7 +15,7 @@ - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. - [sk_num, sk_value, sk_pop, sk_pop_free](../../../core/libs/prx/libSceLibreSsl/src/Stack.cpp) (libSceLibreSsl) - the 32-byte `_STACK` layout (`num`, `data`, `sorted`, `num_alloc`, `comp`) and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/stack) `stack.h` and `stack.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. `sk_pop_free` releases the element array and the stack with the guest `free`, as the module would through libc. No export of this module creates a stack yet, so only stacks the caller builds reach them. A null element array that LibreSSL would read through, and a null free function with a non-null element, throw where LibreSSL faults - [RSA_flags, RSA_size](../../../core/libs/prx/libSceLibreSsl/src/Rsa.cpp) (libSceLibreSsl) - nothing is checked on the console. Layouts and results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto) `rsa.h`, `rsa_crpt.c` and `bn_lib.c`, the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export of this module creates a key yet, so only keys the caller builds reach them. `RSA_size` counts the modulus bytes from its top word, as `BN_num_bytes`, so a zero top word is not skipped. A null method, a null key or modulus, and a positive word count with null words, which LibreSSL dereferences, throw, as do a negative word count and a bit count that overflows `int` -- [X509_STORE_CTX_get_error](../../../core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp) (libSceLibreSsl) - the 240-byte `X509_STORE_CTX` layout and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_vfy.h` and `x509_vfy.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. No export of this module creates or fills a context yet, so only contexts the caller builds reach them. A null context, which LibreSSL dereferences, throws +- [X509_STORE_CTX_get_error, X509_STORE_CTX_set_error](../../../core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp) (libSceLibreSsl) - the 240-byte `X509_STORE_CTX` layout and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_vfy.h` and `x509_vfy.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. No export of this module creates or fills a context yet, so only contexts the caller builds reach them. A null context, which LibreSSL dereferences, throws ### Functional From 1c7856a471426c5d76393f177cc35df0b3a9a7ea Mon Sep 17 00:00:00 2001 From: Alvaro Date: Sat, 24 Oct 2026 02:47:28 +0200 Subject: [PATCH 168/702] feat(libSceLibreSsl): implement X509_STORE_CTX_get_error_depth --- core/libs/prx/libSceLibreSsl/Export.cpp | 5 ----- .../prx/libSceLibreSsl/src/X509StoreContext.cpp | 4 ++++ .../libs/tests/GuestLibreSslX509StoreContext.cpp | 16 ++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 4 ++-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 634199c2eb..fed8e7149d 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1493,11 +1493,6 @@ int APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(void) { return 0; } -int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI X509_STORE_CTX_get_ex_data_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp index e0160983ee..83bc0fb790 100644 --- a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp +++ b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp @@ -54,4 +54,8 @@ void APS5_VABI X509_STORE_CTX_set_error_nid_postfix(LibreSslX509StoreContext* co Context(context, __func__)->error = error; } +int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(LibreSslX509StoreContext* context) { + return Context(context, __func__)->errorDepth; +} + } diff --git a/core/libs/tests/GuestLibreSslX509StoreContext.cpp b/core/libs/tests/GuestLibreSslX509StoreContext.cpp index c1cef4232c..b83ee7cd38 100644 --- a/core/libs/tests/GuestLibreSslX509StoreContext.cpp +++ b/core/libs/tests/GuestLibreSslX509StoreContext.cpp @@ -10,6 +10,7 @@ extern "C" { int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void*); void APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void*, int); +int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(void*); } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -24,6 +25,7 @@ template static void RequireThrows(TAction action, std::source } constexpr std::size_t ContextSize = 240; +constexpr std::size_t ErrorDepthOffset = 180; constexpr std::size_t ErrorOffset = 184; struct alignas(8) TestContext { @@ -71,8 +73,22 @@ static void TestSetError() { RequireThrows([] { X509_STORE_CTX_set_error_nid_postfix(nullptr, 1); }); } +static void TestGetErrorDepth() { + TestContext context; + context.Write(ErrorDepthOffset, 0); + Require(X509_STORE_CTX_get_error_depth_nid_postfix(&context) == 0); + context.Write(ErrorDepthOffset, 3); + Require(X509_STORE_CTX_get_error_depth_nid_postfix(&context) == 3); + context.Write(ErrorOffset, 9); + Require(X509_STORE_CTX_get_error_depth_nid_postfix(&context) == 3); + X509_STORE_CTX_set_error_nid_postfix(&context, 11); + Require(X509_STORE_CTX_get_error_depth_nid_postfix(&context) == 3); + RequireThrows([] { X509_STORE_CTX_get_error_depth_nid_postfix(nullptr); }); +} + int main() { TestGetError(); TestSetError(); + TestGetErrorDepth(); std::puts("libSceLibreSsl X509_STORE_CTX tests passed"); } diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 5022af3091..68e7afe9d2 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 317 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 316 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -15,7 +15,7 @@ - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. - [sk_num, sk_value, sk_pop, sk_pop_free](../../../core/libs/prx/libSceLibreSsl/src/Stack.cpp) (libSceLibreSsl) - the 32-byte `_STACK` layout (`num`, `data`, `sorted`, `num_alloc`, `comp`) and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/stack) `stack.h` and `stack.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. `sk_pop_free` releases the element array and the stack with the guest `free`, as the module would through libc. No export of this module creates a stack yet, so only stacks the caller builds reach them. A null element array that LibreSSL would read through, and a null free function with a non-null element, throw where LibreSSL faults - [RSA_flags, RSA_size](../../../core/libs/prx/libSceLibreSsl/src/Rsa.cpp) (libSceLibreSsl) - nothing is checked on the console. Layouts and results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto) `rsa.h`, `rsa_crpt.c` and `bn_lib.c`, the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export of this module creates a key yet, so only keys the caller builds reach them. `RSA_size` counts the modulus bytes from its top word, as `BN_num_bytes`, so a zero top word is not skipped. A null method, a null key or modulus, and a positive word count with null words, which LibreSSL dereferences, throw, as do a negative word count and a bit count that overflows `int` -- [X509_STORE_CTX_get_error, X509_STORE_CTX_set_error](../../../core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp) (libSceLibreSsl) - the 240-byte `X509_STORE_CTX` layout and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_vfy.h` and `x509_vfy.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. No export of this module creates or fills a context yet, so only contexts the caller builds reach them. A null context, which LibreSSL dereferences, throws +- [X509_STORE_CTX_get_error, X509_STORE_CTX_set_error, X509_STORE_CTX_get_error_depth](../../../core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp) (libSceLibreSsl) - the 240-byte `X509_STORE_CTX` layout and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_vfy.h` and `x509_vfy.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. No export of this module creates or fills a context yet, so only contexts the caller builds reach them. A null context, which LibreSSL dereferences, throws ### Functional From 7ac490f24052d33b2d36ea9ddd243c5799bbd0d8 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Sat, 24 Oct 2026 02:47:31 +0200 Subject: [PATCH 169/702] feat(libSceLibreSsl): implement X509_STORE_CTX_get_current_cert --- core/libs/prx/libSceLibreSsl/Export.cpp | 5 ----- .../prx/libSceLibreSsl/src/X509StoreContext.cpp | 4 ++++ core/libs/tests/GuestLibreSslX509StoreContext.cpp | 14 ++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 4 ++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index fed8e7149d..1ff1e5350c 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1488,11 +1488,6 @@ int APS5_VABI X509_STORE_CTX_get_chain_nid_postfix(void) { return 0; } -int APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI X509_STORE_CTX_get_ex_data_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp index 83bc0fb790..dfe188c18a 100644 --- a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp +++ b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp @@ -58,4 +58,8 @@ int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(LibreSslX509StoreContex return Context(context, __func__)->errorDepth; } +void* APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(LibreSslX509StoreContext* context) { + return Context(context, __func__)->currentCert; +} + } diff --git a/core/libs/tests/GuestLibreSslX509StoreContext.cpp b/core/libs/tests/GuestLibreSslX509StoreContext.cpp index b83ee7cd38..ce71397d63 100644 --- a/core/libs/tests/GuestLibreSslX509StoreContext.cpp +++ b/core/libs/tests/GuestLibreSslX509StoreContext.cpp @@ -11,6 +11,7 @@ extern "C" { int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void*); void APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void*, int); int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(void*); +void* APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(void*); } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -27,6 +28,7 @@ template static void RequireThrows(TAction action, std::source constexpr std::size_t ContextSize = 240; constexpr std::size_t ErrorDepthOffset = 180; constexpr std::size_t ErrorOffset = 184; +constexpr std::size_t CurrentCertOffset = 192; struct alignas(8) TestContext { unsigned char bytes[ContextSize]; @@ -86,9 +88,21 @@ static void TestGetErrorDepth() { RequireThrows([] { X509_STORE_CTX_get_error_depth_nid_postfix(nullptr); }); } +static void TestGetCurrentCert() { + TestContext context; + int certificate = 0; + context.Write(CurrentCertOffset, nullptr); + Require(X509_STORE_CTX_get_current_cert_nid_postfix(&context) == nullptr); + context.Write(CurrentCertOffset, &certificate); + Require(X509_STORE_CTX_get_current_cert_nid_postfix(&context) == &certificate); + Require(OnlyChanged(context, CurrentCertOffset, 8)); + RequireThrows([] { X509_STORE_CTX_get_current_cert_nid_postfix(nullptr); }); +} + int main() { TestGetError(); TestSetError(); TestGetErrorDepth(); + TestGetCurrentCert(); std::puts("libSceLibreSsl X509_STORE_CTX tests passed"); } diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 68e7afe9d2..1e3704eb86 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 316 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 315 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -15,7 +15,7 @@ - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. - [sk_num, sk_value, sk_pop, sk_pop_free](../../../core/libs/prx/libSceLibreSsl/src/Stack.cpp) (libSceLibreSsl) - the 32-byte `_STACK` layout (`num`, `data`, `sorted`, `num_alloc`, `comp`) and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/stack) `stack.h` and `stack.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. `sk_pop_free` releases the element array and the stack with the guest `free`, as the module would through libc. No export of this module creates a stack yet, so only stacks the caller builds reach them. A null element array that LibreSSL would read through, and a null free function with a non-null element, throw where LibreSSL faults - [RSA_flags, RSA_size](../../../core/libs/prx/libSceLibreSsl/src/Rsa.cpp) (libSceLibreSsl) - nothing is checked on the console. Layouts and results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto) `rsa.h`, `rsa_crpt.c` and `bn_lib.c`, the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export of this module creates a key yet, so only keys the caller builds reach them. `RSA_size` counts the modulus bytes from its top word, as `BN_num_bytes`, so a zero top word is not skipped. A null method, a null key or modulus, and a positive word count with null words, which LibreSSL dereferences, throw, as do a negative word count and a bit count that overflows `int` -- [X509_STORE_CTX_get_error, X509_STORE_CTX_set_error, X509_STORE_CTX_get_error_depth](../../../core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp) (libSceLibreSsl) - the 240-byte `X509_STORE_CTX` layout and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_vfy.h` and `x509_vfy.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. No export of this module creates or fills a context yet, so only contexts the caller builds reach them. A null context, which LibreSSL dereferences, throws +- [X509_STORE_CTX_get_error, X509_STORE_CTX_set_error, X509_STORE_CTX_get_error_depth, X509_STORE_CTX_get_current_cert](../../../core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp) (libSceLibreSsl) - the 240-byte `X509_STORE_CTX` layout and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_vfy.h` and `x509_vfy.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. No export of this module creates or fills a context yet, so only contexts the caller builds reach them. A null context, which LibreSSL dereferences, throws ### Functional From 0c037dd9183c6e14014d19319207dc24b538825e Mon Sep 17 00:00:00 2001 From: Alvaro Date: Sat, 24 Oct 2026 02:47:35 +0200 Subject: [PATCH 170/702] feat(libSceLibreSsl): implement X509_STORE_CTX_get_chain --- core/libs/prx/libSceLibreSsl/Export.cpp | 5 ----- .../prx/libSceLibreSsl/src/X509StoreContext.cpp | 4 ++++ core/libs/tests/GuestLibreSslX509StoreContext.cpp | 14 ++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 4 ++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 1ff1e5350c..7f439a457e 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1483,11 +1483,6 @@ int APS5_VABI X509_STORE_CTX_get1_chain_nid_postfix(void) { return 0; } -int APS5_VABI X509_STORE_CTX_get_chain_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI X509_STORE_CTX_get_ex_data_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp index dfe188c18a..bf09832ff1 100644 --- a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp +++ b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp @@ -62,4 +62,8 @@ void* APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(LibreSslX509StoreCon return Context(context, __func__)->currentCert; } +void* APS5_VABI X509_STORE_CTX_get_chain_nid_postfix(LibreSslX509StoreContext* context) { + return Context(context, __func__)->chain; +} + } diff --git a/core/libs/tests/GuestLibreSslX509StoreContext.cpp b/core/libs/tests/GuestLibreSslX509StoreContext.cpp index ce71397d63..b887a60570 100644 --- a/core/libs/tests/GuestLibreSslX509StoreContext.cpp +++ b/core/libs/tests/GuestLibreSslX509StoreContext.cpp @@ -12,6 +12,7 @@ int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void*); void APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void*, int); int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(void*); void* APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(void*); +void* APS5_VABI X509_STORE_CTX_get_chain_nid_postfix(void*); } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -26,6 +27,7 @@ template static void RequireThrows(TAction action, std::source } constexpr std::size_t ContextSize = 240; +constexpr std::size_t ChainOffset = 160; constexpr std::size_t ErrorDepthOffset = 180; constexpr std::size_t ErrorOffset = 184; constexpr std::size_t CurrentCertOffset = 192; @@ -99,10 +101,22 @@ static void TestGetCurrentCert() { RequireThrows([] { X509_STORE_CTX_get_current_cert_nid_postfix(nullptr); }); } +static void TestGetChain() { + TestContext context; + int chain = 0; + context.Write(ChainOffset, nullptr); + Require(X509_STORE_CTX_get_chain_nid_postfix(&context) == nullptr); + context.Write(ChainOffset, &chain); + Require(X509_STORE_CTX_get_chain_nid_postfix(&context) == &chain); + Require(OnlyChanged(context, ChainOffset, 8)); + RequireThrows([] { X509_STORE_CTX_get_chain_nid_postfix(nullptr); }); +} + int main() { TestGetError(); TestSetError(); TestGetErrorDepth(); TestGetCurrentCert(); + TestGetChain(); std::puts("libSceLibreSsl X509_STORE_CTX tests passed"); } diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 1e3704eb86..ebacce178e 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 315 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 314 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -15,7 +15,7 @@ - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. - [sk_num, sk_value, sk_pop, sk_pop_free](../../../core/libs/prx/libSceLibreSsl/src/Stack.cpp) (libSceLibreSsl) - the 32-byte `_STACK` layout (`num`, `data`, `sorted`, `num_alloc`, `comp`) and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/stack) `stack.h` and `stack.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. `sk_pop_free` releases the element array and the stack with the guest `free`, as the module would through libc. No export of this module creates a stack yet, so only stacks the caller builds reach them. A null element array that LibreSSL would read through, and a null free function with a non-null element, throw where LibreSSL faults - [RSA_flags, RSA_size](../../../core/libs/prx/libSceLibreSsl/src/Rsa.cpp) (libSceLibreSsl) - nothing is checked on the console. Layouts and results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto) `rsa.h`, `rsa_crpt.c` and `bn_lib.c`, the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export of this module creates a key yet, so only keys the caller builds reach them. `RSA_size` counts the modulus bytes from its top word, as `BN_num_bytes`, so a zero top word is not skipped. A null method, a null key or modulus, and a positive word count with null words, which LibreSSL dereferences, throw, as do a negative word count and a bit count that overflows `int` -- [X509_STORE_CTX_get_error, X509_STORE_CTX_set_error, X509_STORE_CTX_get_error_depth, X509_STORE_CTX_get_current_cert](../../../core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp) (libSceLibreSsl) - the 240-byte `X509_STORE_CTX` layout and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_vfy.h` and `x509_vfy.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. No export of this module creates or fills a context yet, so only contexts the caller builds reach them. A null context, which LibreSSL dereferences, throws +- [X509_STORE_CTX_get_error, X509_STORE_CTX_set_error, X509_STORE_CTX_get_error_depth, X509_STORE_CTX_get_current_cert, X509_STORE_CTX_get_chain](../../../core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp) (libSceLibreSsl) - the 240-byte `X509_STORE_CTX` layout and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_vfy.h` and `x509_vfy.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. No export of this module creates or fills a context yet, so only contexts the caller builds reach them. A null context, which LibreSSL dereferences, throws ### Functional From d0927e2302a1b736d98ec5715a703d3ec9e2607d Mon Sep 17 00:00:00 2001 From: Alvaro Date: Sun, 25 Oct 2026 06:00:56 +0100 Subject: [PATCH 171/702] refactor(libSceLibreSsl): declare the X509_STORE_CTX exports in the public header --- core/libs/prx/libSceLibreSsl/include/Api.hpp | 35 ++++++++++++--- .../libSceLibreSsl/src/X509StoreContext.cpp | 26 +---------- .../tests/GuestLibreSslX509StoreContext.cpp | 45 +++++++++---------- 3 files changed, 52 insertions(+), 54 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index a114265d0c..4bad98e531 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -114,6 +114,31 @@ struct LibreSslRsa { void* mtBlinding; }; +struct LibreSslX509StoreContext { + void* store; + int currentMethod; + void* cert; + void* untrusted; + void* crls; + void* param; + void* otherContext; + void* callbacks[12]; + int valid; + int lastUntrusted; + void* chain; + void* tree; + int explicitPolicy; + int errorDepth; + int error; + void* currentCert; + void* currentIssuer; + void* currentCrl; + int currentCrlScore; + unsigned int currentReasons; + LibreSslX509StoreContext* parent; + void* exData; +}; + extern "C" { std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const Asn1String* value); @@ -434,12 +459,12 @@ int APS5_VABI X509_NAME_get_text_by_NID_nid_postfix(void); int APS5_VABI X509_NAME_oneline_nid_postfix(void); int APS5_VABI X509_NAME_print_ex_nid_postfix(void); int APS5_VABI X509_STORE_CTX_get1_chain_nid_postfix(void); -int APS5_VABI X509_STORE_CTX_get_chain_nid_postfix(void); -int APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(void); -int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void); -int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(void); +void* APS5_VABI X509_STORE_CTX_get_chain_nid_postfix(LibreSslX509StoreContext* context); +void* APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(LibreSslX509StoreContext* context); +int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(LibreSslX509StoreContext* context); +int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(LibreSslX509StoreContext* context); int APS5_VABI X509_STORE_CTX_get_ex_data_nid_postfix(void); -int APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void); +void APS5_VABI X509_STORE_CTX_set_error_nid_postfix(LibreSslX509StoreContext* context, int error); int APS5_VABI X509_STORE_add_cert_nid_postfix(void); int APS5_VABI X509_STORE_add_crl_nid_postfix(void); int APS5_VABI X509_STORE_add_lookup_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp index bf09832ff1..e028e86c4e 100644 --- a/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp +++ b/core/libs/prx/libSceLibreSsl/src/X509StoreContext.cpp @@ -1,33 +1,9 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" #include #include #include #include "prx/libc/include/General.hpp" -struct LibreSslX509StoreContext { - void* store; - int currentMethod; - void* cert; - void* untrusted; - void* crls; - void* param; - void* otherContext; - void* callbacks[12]; - int valid; - int lastUntrusted; - void* chain; - void* tree; - int explicitPolicy; - int errorDepth; - int error; - void* currentCert; - void* currentIssuer; - void* currentCrl; - int currentCrlScore; - unsigned int currentReasons; - LibreSslX509StoreContext* parent; - void* exData; -}; - static_assert(sizeof(LibreSslX509StoreContext) == 240); static_assert(offsetof(LibreSslX509StoreContext, chain) == 160); static_assert(offsetof(LibreSslX509StoreContext, errorDepth) == 180); diff --git a/core/libs/tests/GuestLibreSslX509StoreContext.cpp b/core/libs/tests/GuestLibreSslX509StoreContext.cpp index b887a60570..edc80af33b 100644 --- a/core/libs/tests/GuestLibreSslX509StoreContext.cpp +++ b/core/libs/tests/GuestLibreSslX509StoreContext.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -7,14 +8,6 @@ #include #include -extern "C" { -int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void*); -void APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void*, int); -int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(void*); -void* APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(void*); -void* APS5_VABI X509_STORE_CTX_get_chain_nid_postfix(void*); -} - static void Require(bool condition, std::source_location location = std::source_location::current()) { if (condition) return; std::fprintf(stderr, "LibreSSL X509_STORE_CTX check failed at line %u\n", location.line()); @@ -46,6 +39,10 @@ struct alignas(8) TestContext { } }; +static LibreSslX509StoreContext* Context(TestContext& context) { + return reinterpret_cast(context.bytes); +} + static bool OnlyChanged(const TestContext& context, std::size_t offset, std::size_t size) { for (std::size_t index = 0; index < ContextSize; ++index) if ((index < offset || index >= offset + size) && context.bytes[index] != 0xAA) return false; @@ -55,23 +52,23 @@ static bool OnlyChanged(const TestContext& context, std::size_t offset, std::siz static void TestGetError() { TestContext context; context.Write(ErrorOffset, 0); - Require(X509_STORE_CTX_get_error_nid_postfix(&context) == 0); + Require(X509_STORE_CTX_get_error_nid_postfix(Context(context)) == 0); context.Write(ErrorOffset, 20); - Require(X509_STORE_CTX_get_error_nid_postfix(&context) == 20); + Require(X509_STORE_CTX_get_error_nid_postfix(Context(context)) == 20); context.Write(ErrorOffset, -7); - Require(X509_STORE_CTX_get_error_nid_postfix(&context) == -7); + Require(X509_STORE_CTX_get_error_nid_postfix(Context(context)) == -7); Require(OnlyChanged(context, ErrorOffset, 4)); RequireThrows([] { X509_STORE_CTX_get_error_nid_postfix(nullptr); }); } static void TestSetError() { TestContext context; - X509_STORE_CTX_set_error_nid_postfix(&context, 10); + X509_STORE_CTX_set_error_nid_postfix(Context(context), 10); Require(context.Read(ErrorOffset) == 10); - Require(X509_STORE_CTX_get_error_nid_postfix(&context) == 10); - X509_STORE_CTX_set_error_nid_postfix(&context, -1); + Require(X509_STORE_CTX_get_error_nid_postfix(Context(context)) == 10); + X509_STORE_CTX_set_error_nid_postfix(Context(context), -1); Require(context.Read(ErrorOffset) == -1); - X509_STORE_CTX_set_error_nid_postfix(&context, 0); + X509_STORE_CTX_set_error_nid_postfix(Context(context), 0); Require(context.Read(ErrorOffset) == 0); Require(OnlyChanged(context, ErrorOffset, 4)); RequireThrows([] { X509_STORE_CTX_set_error_nid_postfix(nullptr, 1); }); @@ -80,13 +77,13 @@ static void TestSetError() { static void TestGetErrorDepth() { TestContext context; context.Write(ErrorDepthOffset, 0); - Require(X509_STORE_CTX_get_error_depth_nid_postfix(&context) == 0); + Require(X509_STORE_CTX_get_error_depth_nid_postfix(Context(context)) == 0); context.Write(ErrorDepthOffset, 3); - Require(X509_STORE_CTX_get_error_depth_nid_postfix(&context) == 3); + Require(X509_STORE_CTX_get_error_depth_nid_postfix(Context(context)) == 3); context.Write(ErrorOffset, 9); - Require(X509_STORE_CTX_get_error_depth_nid_postfix(&context) == 3); - X509_STORE_CTX_set_error_nid_postfix(&context, 11); - Require(X509_STORE_CTX_get_error_depth_nid_postfix(&context) == 3); + Require(X509_STORE_CTX_get_error_depth_nid_postfix(Context(context)) == 3); + X509_STORE_CTX_set_error_nid_postfix(Context(context), 11); + Require(X509_STORE_CTX_get_error_depth_nid_postfix(Context(context)) == 3); RequireThrows([] { X509_STORE_CTX_get_error_depth_nid_postfix(nullptr); }); } @@ -94,9 +91,9 @@ static void TestGetCurrentCert() { TestContext context; int certificate = 0; context.Write(CurrentCertOffset, nullptr); - Require(X509_STORE_CTX_get_current_cert_nid_postfix(&context) == nullptr); + Require(X509_STORE_CTX_get_current_cert_nid_postfix(Context(context)) == nullptr); context.Write(CurrentCertOffset, &certificate); - Require(X509_STORE_CTX_get_current_cert_nid_postfix(&context) == &certificate); + Require(X509_STORE_CTX_get_current_cert_nid_postfix(Context(context)) == &certificate); Require(OnlyChanged(context, CurrentCertOffset, 8)); RequireThrows([] { X509_STORE_CTX_get_current_cert_nid_postfix(nullptr); }); } @@ -105,9 +102,9 @@ static void TestGetChain() { TestContext context; int chain = 0; context.Write(ChainOffset, nullptr); - Require(X509_STORE_CTX_get_chain_nid_postfix(&context) == nullptr); + Require(X509_STORE_CTX_get_chain_nid_postfix(Context(context)) == nullptr); context.Write(ChainOffset, &chain); - Require(X509_STORE_CTX_get_chain_nid_postfix(&context) == &chain); + Require(X509_STORE_CTX_get_chain_nid_postfix(Context(context)) == &chain); Require(OnlyChanged(context, ChainOffset, 8)); RequireThrows([] { X509_STORE_CTX_get_chain_nid_postfix(nullptr); }); } From a0eaec661118a9093433d1a21b3ccf8b714ebd4e Mon Sep 17 00:00:00 2001 From: Alvaro Date: Mon, 26 Oct 2026 22:30:37 +0100 Subject: [PATCH 172/702] fix(libkernel): fail pthread_condattr_getclock with EINVAL for a null attribute pthread_condattr_getclock threw for a null attribute pointer or a null handle. FreeBSD 11.4 libthr (lib/libthr/thread/thr_condattr.c) returns EINVAL for both before reading the clock, as setclock, setpshared, getpshared and destroy here already do. A null output pointer with a valid attribute faults on FreeBSD, so it still throws. --- core/libs/prx/libkernel/Pthread/Posix/Cond.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index 4c6d721513..35db4e3053 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -73,7 +73,8 @@ int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, Kerne } int APS5_VABI pthread_condattr_getclock_nid_postfix(const PthreadCondattr* attr, KernelClockid* clockId) { - if (!attr || !*attr || !clockId) NotImplemented_nid_no_patch(__func__); + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; + if (!clockId) NotImplemented_nid_no_patch(__func__); *clockId = (*attr)->_clockid; return 0; } From c28f4ef1578e55ba6297220dc5771ecb5c4a276f Mon Sep 17 00:00:00 2001 From: Alvaro Date: Mon, 26 Oct 2026 22:31:01 +0100 Subject: [PATCH 173/702] test(libkernel): check that pthread_condattr_getclock fails with EINVAL for a null attribute A null attribute pointer and a null or destroyed handle return EINVAL and leave the output untouched, also with a null output pointer, which FreeBSD never reads after the attribute check. A null output pointer with a valid attribute still throws. --- core/libs/tests/GuestPthreadCondattr.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/libs/tests/GuestPthreadCondattr.cpp b/core/libs/tests/GuestPthreadCondattr.cpp index 208bbb7776..0aab5e73df 100644 --- a/core/libs/tests/GuestPthreadCondattr.cpp +++ b/core/libs/tests/GuestPthreadCondattr.cpp @@ -35,10 +35,10 @@ static void CheckClockRefused(const PthreadCondattr* attr, KernelClockid* clockI int main() { PthreadCondattr unset = nullptr; KernelClockid clockId = -1; - CheckClockRefused(nullptr, &clockId); - Require(clockId == -1); - CheckClockRefused(&unset, &clockId); - Require(clockId == -1); + Require(pthread_condattr_getclock_nid_postfix(nullptr, &clockId) == GuestEinval && clockId == -1); + Require(pthread_condattr_getclock_nid_postfix(&unset, &clockId) == GuestEinval && clockId == -1); + Require(pthread_condattr_getclock_nid_postfix(nullptr, nullptr) == GuestEinval); + Require(pthread_condattr_getclock_nid_postfix(&unset, nullptr) == GuestEinval); int sharing = -1; Require(scePthreadCondattrSetclock(nullptr, Monotonic) == SceEinval); Require(scePthreadCondattrSetclock(&unset, Monotonic) == SceEinval); @@ -70,8 +70,7 @@ int main() { CheckClock(&other, Monotonic); CheckClock(&attr, Monotonic); Require(pthread_condattr_destroy_nid_postfix(&other) == 0 && other == nullptr); - CheckClockRefused(&other, &clockId); - Require(clockId == -1); + Require(pthread_condattr_getclock_nid_postfix(&other, &clockId) == GuestEinval && clockId == -1); Require(scePthreadCondattrSetpshared(&attr, 1) == SceEinval); Require(scePthreadCondattrSetpshared(&attr, 0) == SCE_OK); Require(scePthreadCondattrGetpshared(&attr, &sharing) == SCE_OK && sharing == 0); @@ -99,8 +98,7 @@ int main() { } Require(threw); Require(scePthreadCondattrDestroy(&attr) == SCE_OK && attr == nullptr); - CheckClockRefused(&attr, &clockId); - Require(clockId == -1); + Require(pthread_condattr_getclock_nid_postfix(&attr, &clockId) == GuestEinval && clockId == -1); Require(scePthreadCondattrDestroy(&attr) == SceEinval); sharing = -1; Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == GuestEinval && sharing == -1); From 16fff3e1a00b02a70422427090848aa9fb3fa5a5 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Mon, 26 Oct 2026 22:31:05 +0100 Subject: [PATCH 174/702] docs(libkernel): narrow the pthread_condattr_getclock debt to a null output pointer --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 34b34b71f8..7df67cdfed 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -39,7 +39,7 @@ - [sceKernelConvertUtcToLocaltime and sceKernelConvertLocaltimeToUtc, measured](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) - a payload process on a PS5 (firmware 13.60) set to UTC-3 with Brazil's history differs from AnyPS5 in two ways. The 16-byte block's first 8 bytes hold the zone's last transition at or before the instant (`1550368800`, the end of summer time in February 2019, for every 2026 and 2038 date; `-57967200` for 1970), not the instant. The last argument is 3600 in every case but `sceKernelConvertLocaltimeToUtc` at 1970-01-01 (0), even in 2026, when the zone has had no summer time since 2019. The offsets, `dst_sec` and the converted times match the zone's history, and both calls return 0 with null outputs. Reproducing the transition needs the zone's full history, which the Windows C runtime does not have - [sceKernelGettimezone](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) reports the host zone, not the console's time zone setting: `tz_minuteswest` holds the standard offset in minutes east of UTC, despite its name, and `tz_dsttime` the zone's daylight saving amount in minutes, found as for the last argument of `sceKernelConvertLocaltimeToUtc`. A PS5 on firmware 13.40 set to US Eastern returned -300 and 60 (measured 2026-10-19, in summer time); what it returns outside summer time or for a zone without daylight saving was not measured. The Windows limits of the entry above apply. - [getuid, geteuid, getgid, getegid, getresuid, getresgid and issetugid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. `getresuid` and `getresgid` follow FreeBSD 11.4's `sys_getresuid` and `sys_getresgid`, writing 0 to non-null pointers and leaving errno untouched on success. The values a title sees on a console are unverified -- [pthread_condattr_getclock](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) - null attribute pointers, null handles and a null output pointer throw; their console error or fault behavior is unverified. The signature follows the public PS5 payload SDK's [pthread.h](https://github.com/ps5-payload-dev/sdk/blob/f5d40f02770e9b0001dd9e18056eca70b3da8b40/include/freebsd/pthread.h). Valid initialized attributes report the stored clock ID, as in [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_condattr.c); this has not been measured on a PS5. Inaccessible non-null pointers and stale handles are not validated. +- [pthread_condattr_getclock](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) - a null output pointer with a valid attribute throws; FreeBSD faults there and the console behavior is unverified. Null attribute pointers and null handles fail with EINVAL as in FreeBSD, not measured on a PS5. The signature follows the public PS5 payload SDK's [pthread.h](https://github.com/ps5-payload-dev/sdk/blob/f5d40f02770e9b0001dd9e18056eca70b3da8b40/include/freebsd/pthread.h). Valid initialized attributes report the stored clock ID, as in [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_condattr.c); this has not been measured on a PS5. Inaccessible non-null pointers and stale handles are not validated. - [sceKernelGetAppInfo](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer. - [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. - [APR resolve ForEach result](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - `sceKernelAprResolveFilepathsToIdsAndFileSizesForEach` returns the number of paths that resolved, with the per-path status in `results` and the failed ids/sizes set to the invalid id and 0. The [ampr_emu](https://github.com/drakmor/ampr_emu) bridge passes the count of its resolver through and does not show what is counted; counting successes is not confirmed on a title. The guest errno of the failing non-`ForEach` resolves is set through `__error_nid_postfix` From 7f3f58284c6c8657110b0ee838a593d31d2e1682 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 27 Oct 2026 10:36:36 +1300 Subject: [PATCH 175/702] fix(agc): serve host imports by lookups until two frames have ended Before two flips have ended a frame nothing was evictable, so a cap retry was refused and aborted where main served it. Until then imports age by lookups as before; once frames exist the frame rule applies. A frame end now moves its own counter, used by the evicting-refusal memory, not the imports epoch, so the address-space cache survives a flip. --- .../Graphics/include/GuestBufferMemory.hpp | 2 + .../Graphics/src/GuestBufferMemory.cpp | 44 +++++++++++++++---- .../libSceAgcDriver/tests/BdaResources.cpp | 10 +++-- .../prx/libSceAgcDriver/tests/BdaTests.hpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 6 +-- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 10 +++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 7 files changed, 59 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 99a5a3274b..ad7ca42161 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -119,6 +119,8 @@ void SetHostImportMaxBytes(std::uint64_t bytes); std::uint64_t HostImportLiveBytes(); // A flip ends a frame: an import used in the current or the previous frame is never evicted for another one. void NoteHostImportFrame(); +std::uint64_t HostImportFramesEnded(); +std::uint64_t HostImportEpoch(); std::uint64_t HostImportBudgetRemaining(const Context& context); struct HostImportSettleCounts { std::uint64_t submitted = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 0b37c2e454..57ecbe926b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -184,6 +184,7 @@ struct EvictingRefusal { std::uint64_t bytes; GuestAllocations::Mapped ranges; std::uint64_t epoch; + std::uint64_t frames; std::uint64_t liveBytes; std::uint64_t budget; std::uint64_t room; @@ -276,6 +277,7 @@ struct HostImports { std::uint64_t buildFloor = 0; std::uint64_t frameFloor = 0; std::uint64_t previousFrameFloor = 0; + std::uint64_t framesEnded = 0; std::array counts{}; VkPhysicalDevice reportedDevice = VK_NULL_HANDLE; std::chrono::steady_clock::time_point reportedAt{}; @@ -1485,12 +1487,20 @@ bool pointerHeld(const HostImports& state, const HostImport& entry) { return effectiveUse(state, entry) >= state.buildFloor; } +constexpr std::uint64_t ProtectedUses = 256; + +bool hasFrameHistory(const HostImports& state) { + return state.previousFrameFloor != 0; +} + bool evictable(const HostImports& state, const HostImport& entry) { - return !pointerHeld(state, entry) && effectiveUse(state, entry) < state.previousFrameFloor; + if (pointerHeld(state, entry)) return false; + if (!hasFrameHistory(state)) return effectiveUse(state, entry) + ProtectedUses < state.useCounter; + return effectiveUse(state, entry) < state.previousFrameFloor; } bool evictingRefusalCurrent(const HostImports& state, const EvictingRefusal& refusal, std::uint64_t budget) { - return refusal.epoch == state.epoch && refusal.liveBytes == state.liveBytes && budget <= refusal.budget && refusal.window == importWindow.load(std::memory_order_relaxed) && state.useCounter < refusal.ready && (state.buildFloor <= refusal.floor || state.useCounter < refusal.readyHeld); + return refusal.epoch == state.epoch && refusal.frames == state.framesEnded && refusal.liveBytes == state.liveBytes && budget <= refusal.budget && refusal.window == importWindow.load(std::memory_order_relaxed) && state.useCounter < refusal.ready && (state.buildFloor <= refusal.floor || state.useCounter < refusal.readyHeld); } void noteRetiredUnwatched(std::vector>& spans, const HostImport& entry, const GuestAllocations::Lease& lease) { @@ -1661,9 +1671,10 @@ const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std: const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end); const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict, ImportRequester requester); -// The bytes retireLeastRecent could free outside [begin, end). readyHeld is 0 when an import a build still holds is already -// two frames old: it becomes evictable as soon as that build's floor rises, which ends a remembered refusal. -std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end, std::uint64_t& readyHeld) { +// The bytes retireLeastRecent could free outside [begin, end). Before two frames have ended the imports age by lookups as they +// did before frames existed. After that readyHeld is 0 when an import a build still holds is already two frames old: it becomes +// evictable as soon as that build's floor rises, which ends a remembered refusal. +std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end, std::uint64_t& ready, std::uint64_t& readyHeld) { std::uint64_t bytes = 0; for (const auto& [base, entry] : state.imports) { if (entry.base + entry.bytes > begin && entry.base < end) continue; @@ -1671,7 +1682,10 @@ std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std: bytes += entry.pinnedBytes; continue; } - if (pointerHeld(state, entry) && effectiveUse(state, entry) < state.previousFrameFloor) readyHeld = 0; + if (!hasFrameHistory(state)) { + auto& horizon = pointerHeld(state, entry) ? readyHeld : ready; + horizon = std::min(horizon, effectiveUse(state, entry) + ProtectedUses + 1); + } else if (pointerHeld(state, entry) && effectiveUse(state, entry) < state.previousFrameFloor) readyHeld = 0; } return bytes; } @@ -1861,7 +1875,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import const auto live = state.liveBytes - replaced; auto ready = std::numeric_limits::max(); auto readyHeld = std::numeric_limits::max(); - const auto evictable = evict && live + bytes > budget ? evictableBytes(state, base, base + bytes, readyHeld) : 0; + const auto evictable = evict && live + bytes > budget ? evictableBytes(state, base, base + bytes, ready, readyHeld) : 0; if (live + bytes > budget + evictable) { // A refused import turns every later use of the range into CPU copies, so say so. static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); @@ -1884,7 +1898,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (%s); %llu imports (%llu MiB) refused so far, %llu of them remembered, last 0x%llx+0x%llx by %s\n", static_cast(state.liveBytes >> 20u), static_cast(budget >> 20u), budgetSource(context, state, budget).c_str(), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(remembered), static_cast(base), static_cast(bytes), ImportRequesterName(requester)); } const auto room = budget + evictable > live ? budget + evictable - live : 0; - if (evict) state.evictingRefusals.insert_or_assign(std::pair{base, bytes}, EvictingRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), state.epoch, state.liveBytes, budget, room, importWindow.load(std::memory_order_relaxed), state.buildFloor, ready, readyHeld, joined}); + if (evict) state.evictingRefusals.insert_or_assign(std::pair{base, bytes}, EvictingRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), state.epoch, state.framesEnded, state.liveBytes, budget, room, importWindow.load(std::memory_order_relaxed), state.buildFloor, ready, readyHeld, joined}); if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room)); if (containing != run.ranges.end() && run.ranges.size() > 1) remember((*containing)->address, (*containing)->bytes, GuestAllocations::Mapped{*containing}); ++state.budgetRefusals; @@ -3730,7 +3744,19 @@ void NoteHostImportFrame() { std::lock_guard lock(state.mutex); state.previousFrameFloor = state.frameFloor; state.frameFloor = state.useCounter + 1; - ++state.epoch; + ++state.framesEnded; +} + +std::uint64_t HostImportEpoch() { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return state.epoch; +} + +std::uint64_t HostImportFramesEnded() { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return state.framesEnded; } void SetHostImportBudget(std::uint64_t bytes) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 1ce3cce190..e53aa801b4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1163,8 +1163,13 @@ void importEvictionFramesTests(const Context& context) { for (int lookup = 0; lookup < 300; ++lookup) static_cast(HostImportFor(importing, at[index], 16)); return HostImportSerial(importing, at[index], 16, false); }; + NoteHostImportFrame(); + NoteHostImportFrame(); std::array serials{use(0), use(1)}; Require(serials[0] != 0 && serials[1] != 0, "two ranges under a budget for two were not imported"); + const auto epoch = HostImportEpoch(); + NoteHostImportFrame(); + Require(HostImportEpoch() == epoch, "a frame end with no import change moved the imports epoch"); for (int frame = 0; frame < 10; ++frame) { Require(use(2) == 0, "a range over the budget evicted an import used in the same frame (frame " + std::to_string(frame) + ")"); NoteHostImportFrame(); @@ -3544,7 +3549,7 @@ void RunHeapMirrorRunUnwatchTests(const Context& context, const BdaTestAccess& a std::cout << "an address-based build refreshes the unwatched heap of a run in full and its watched neighbour by its writes\n"; } -void RunAddressCopyCapTests(const Context& context, const BdaTestAccess& access, bool remembered) { +void RunAddressCopyCapTests(const Context& context, const BdaTestAccess& access, bool remembered, int frames) { constexpr std::size_t block = 65536; constexpr std::size_t heapBytes = 32 * block; constexpr std::size_t idleBytes = 16 * block; @@ -3639,8 +3644,7 @@ void RunAddressCopyCapTests(const Context& context, const BdaTestAccess& access, SetAddressCopyLimit(0); unrelated(true); } - NoteHostImportFrame(); - NoteHostImportFrame(); + for (int frame = 0; frame < frames; ++frame) NoteHostImportFrame(); for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, busy, 64) != nullptr, "the busy range lost its import"); if (!remembered) unwatch(); const auto refused = builds(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 7479c1f4b1..74b45a2581 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -21,7 +21,7 @@ void RunHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, cons void RunSparseHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunSparseHeapMirrorBudgetTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunHeapMirrorRunUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); -void RunAddressCopyCapTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool remembered); +void RunAddressCopyCapTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool remembered, int frames); void RunGuestAllocationTests(); void RunGuestLeaseWaitTests(); void RunUnmappedGapTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 335a88d0dd..11878dea25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -12298,7 +12298,7 @@ int main(int argc, char** argv) { #else setenv("APS5_ADDRESS_COPY_MAX_MIB", "1", 1); #endif - { + for (const int frames : {0, 1, 2}) { mock = MockVulkan{}; auto context = mockContext(); context.bufferDeviceAddress = true; @@ -12313,9 +12313,9 @@ int main(int argc, char** argv) { }, {}, {} - }, std::string_view(argv[1]) == "--address-copy-cap-remembered"); + }, std::string_view(argv[1]) == "--address-copy-cap-remembered", frames); + Require(mock.live == 0, "the address copy cap test leaked Vulkan objects"); } - Require(mock.live == 0, "the address copy cap test leaked Vulkan objects"); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") { diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index fa67366d65..d42e95c990 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libc/include/GuestHeap.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" @@ -208,6 +209,7 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip-only queue submit failed"); AgcDriverWaitIdle_nid_postfix(); check(output->ready.load() == 1, "flip-only queue flip was not handed to its output"); + check(AgcDriver::Graphics::HostImportFramesEnded() == 1, "the first flip did not end a host import frame"); check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip on a queue that never requested a release interrupt did not raise its event"); check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip-only queue event was not cleared"); std::array words{0xc0064900, 0, 1u << 24u, 0, 0, 0, 0, 0}; @@ -229,6 +231,7 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after a release interrupt submit failed"); AgcDriverWaitIdle_nid_postfix(); check(output->ready.load() == 2, "flip after a release interrupt was not handed to its output"); + check(AgcDriver::Graphics::HostImportFramesEnded() == 2, "a flip packet executed on the queue did not end a host import frame"); check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip on a queue that already raised its own release interrupt raised a second event"); words[2] = 1u << 24u; check(sceAgcDriverSubmitDcb(&packet) == 0, "release before sustained flips submit failed"); @@ -259,6 +262,13 @@ void testEndOfPipeInterrupts() { check(output->ready.load() == static_cast(4 + sustainedFlips + index), "flip paired with a release interrupt was not handed to its output"); check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a flip raised an event within two flips of the queue's own release interrupt"); } + const auto framesBefore = AgcDriver::Graphics::HostImportFramesEnded(); + const auto readyBefore = output->ready.load(); + std::thread([] { AgcDriverSubmitFlip_nid_postfix(AgcDriver::FlipInfo{7, -1, 1, 0}); }).join(); + AgcDriverWaitIdle_nid_postfix(); + static_cast(owner->GetTriggeredEvents(events.data(), 2)); + check(output->ready.load() == readyBefore + 1, "flip from a thread that never submitted queue 0 was not handed to its output"); + check(AgcDriver::Graphics::HostImportFramesEnded() == framesBefore + 1, "a CPU flip from a thread that never submitted queue 0 did not end a host import frame"); AgcDriverUnregisterVideoOutput_nid_postfix(7, output); alignas(8) static volatile std::uint64_t label = 0; const auto labelAddress = reinterpret_cast(&label); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c6f20c73be..30ce4a7177 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -247,7 +247,7 @@ - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. -- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the current or the previous frame (a frame ends at a flip packet or a CPU flip), so a set used in every frame keeps its imports and what does not fit is copied, as below the budget; without flips (a loading stretch, compute-only work) no frame ends and nothing is evictable, so imports that do not fit are copied instead of evicting by recency; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: no frame has ended, and no build holding an import two frames old has raised its floor. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the two-frame protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. +- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the current or the previous frame (a frame ends at a flip packet or a CPU flip), so a set used in every frame keeps its imports and what does not fit is copied, as below the budget; until two frames have ended imports age by lookups, an import being evictable 256 lookups after its last use; after that, without flips (a loading stretch, compute-only work) no frame ends and nothing more becomes evictable, so imports that do not fit are copied instead of evicting by recency; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: no frame has ended (or, before two frames have ended, 256 lookups have not passed), and no build holding an import two frames old has raised its floor. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the two-frame protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. - [Piece-split ranges over the host import budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): an address-based build imports a registered range larger than the device's largest allocation in pieces. If the budget refuses a piece, the range is mirrored or copied without the evicting retry other refused ranges get when their copy would pass `APS5_ADDRESS_COPY_MAX_MIB`, so such a copy can still exceed that cap and end the build. - [Device-local staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written or atomic buffer element inside a host import, including one an address-based compute build reaches through its pointers, is copied to device memory before the dispatch and copied back whole after it, so a CPU store into that range in between is rolled back. - [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. From 0b3b2d846c03d75b1e60e2baba07fc36680b23f4 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Mon, 26 Oct 2026 16:25:16 +0530 Subject: [PATCH 176/702] feat(libSceVrTracker2): identify sceVrTracker2GetRawCameraMeshSize export --- core/libs/prx/libSceVrTracker2/Export.cpp | 5 ++--- core/libs/prx/libSceVrTracker2/include/Api.hpp | 2 +- core/libs/tests/GuestVrTracker2.cpp | 3 +++ docs/dev/debt/libSceVrTracker2.md | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index f9246da310..7a58f2152c 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -652,9 +652,8 @@ std::int32_t APS5_VABI VrTracker2Unknown01(VrTracker2CalibrationResult* result, return 0; } -APS5_EXPORT("NBhWy+Gl0tY", VrTracker2Unknown02); -std::int32_t APS5_VABI VrTracker2Unknown02(VrTracker2MeshRequirement* requirement, std::uint32_t format) { - NotImplemented_nid_no_patch("NBhWy+Gl0tY"); +std::int32_t APS5_VABI sceVrTracker2GetRawCameraMeshSize(VrTracker2MeshRequirement* requirement, std::uint32_t format) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVrTracker2/include/Api.hpp b/core/libs/prx/libSceVrTracker2/include/Api.hpp index 871f89b821..3851bc7a44 100644 --- a/core/libs/prx/libSceVrTracker2/include/Api.hpp +++ b/core/libs/prx/libSceVrTracker2/include/Api.hpp @@ -300,7 +300,7 @@ std::int32_t APS5_VABI VrTracker2Accept(); std::int32_t APS5_VABI VrTracker2FillDefaults(void* output); std::int32_t APS5_VABI VrTracker2ClearWord(void* output); std::int32_t APS5_VABI VrTracker2Unknown01(VrTracker2CalibrationResult* result, std::uint32_t capacity); -std::int32_t APS5_VABI VrTracker2Unknown02(VrTracker2MeshRequirement* requirement, std::uint32_t format); +std::int32_t APS5_VABI sceVrTracker2GetRawCameraMeshSize(VrTracker2MeshRequirement* requirement, std::uint32_t format); std::int32_t APS5_VABI VrTracker2Unknown03(std::uint32_t mode, void* output, std::size_t capacity, std::size_t* writtenSize); std::int32_t APS5_VABI VrTracker2Unknown04(const VrTracker2ResultParameter* parameter, VrTracker2Result* result); std::int32_t APS5_VABI VrTracker2Unknown05(VrTracker2Pose* pose, std::uint64_t coordinateId); diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp index a1e4b0693a..392bd122f5 100644 --- a/core/libs/tests/GuestVrTracker2.cpp +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -247,6 +247,9 @@ int main() { parameter[0] = 0x21; Require(sceVrTracker2QueryMemory(reinterpret_cast(parameter.data()), reinterpret_cast(requirement.data())) == InvalidArgument, "query size"); + VrTracker2MeshRequirement meshRequirement{}; + Require(Throws([&] { sceVrTracker2GetRawCameraMeshSize(&meshRequirement, 0); }), "mesh size query throws unimplemented"); + std::vector memory(TrackerMemory + WorkMemoryTail); CheckInitializeValidation(memory); const auto previous = std::filesystem::current_path(); diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 5bb664cbf1..330978dc78 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -7,7 +7,7 @@ ### Unknown function info - [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - never initializes: after validation it returns the first non-zero libSceHmd2 tracking or LED-detector open/start result and resets to uninitialized, and throws if all four succeed or for modes 1000/1001, whose application category the host lacks. Unverified: the mode 0 category query is assumed to succeed, the `sceKernelGetAppInfo2` attribute checks are skipped (the host has no such record; a zeroed one passes), the daemon IPC client and work-memory heap are not created, rollback only undoes what the call opened, and the meanings of the options at 0x30, 0x38 and 0x39 remain unknown (0x39 is only range-checked). The behaviour is not measured on a console; the -1 a no-headset host gets relies on libSceHmd2's measured pre-initialization answers. PPSA05666 calls it with no headset and, with #7463, #7690 and #7658 applied, stopped on the NotImplemented. -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the anonymous throwing exports have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (identified as sceVrTracker2GetRawCameraMeshSize, `NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. - [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two coordinate-system selectors and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. From 2c3c037b8989ea6220bd2242675d955fdf797919 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Tue, 27 Oct 2026 03:08:10 +0530 Subject: [PATCH 177/702] test(libSceVrTracker2): drop unimplemented throw test for sceVrTracker2GetRawCameraMeshSize --- core/libs/tests/GuestVrTracker2.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp index 392bd122f5..a1e4b0693a 100644 --- a/core/libs/tests/GuestVrTracker2.cpp +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -247,9 +247,6 @@ int main() { parameter[0] = 0x21; Require(sceVrTracker2QueryMemory(reinterpret_cast(parameter.data()), reinterpret_cast(requirement.data())) == InvalidArgument, "query size"); - VrTracker2MeshRequirement meshRequirement{}; - Require(Throws([&] { sceVrTracker2GetRawCameraMeshSize(&meshRequirement, 0); }), "mesh size query throws unimplemented"); - std::vector memory(TrackerMemory + WorkMemoryTail); CheckInitializeValidation(memory); const auto previous = std::filesystem::current_path(); From 831308c6815e4d2b6342e5ea3a46922d7d506a40 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:53:47 +0000 Subject: [PATCH 178/702] fix(tools): keep pull requests whose paths the base renamed or deleted in pr_overlap's branch probe --- tools/pr_overlap.py | 17 +++++++++++------ tools/tests/test_pr_overlap_progress.py | 5 +++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index c655dd6e88..b761d2a6c3 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -330,20 +330,25 @@ def scan(pr): def probe(pr): - changes = git("diff", "--name-only", "--no-renames", "-z", f"refs/pr/base...{pr['ref']}").split("\0") - files = {path for path in changes if path} + changes = git("diff", "--name-status", "--no-renames", "-z", f"refs/pr/base...{pr['ref']}").split("\0") + files, existing = set(), set() + for status, path in zip(changes[::2], changes[1::2]): + files.add(path) + if status != "A": + existing.add(path) if not any(path.startswith("core/libs/prx/") and path.endswith(".cpp") for path in files): - return files, set() - return files, exports(git("diff", "-U0", "--no-color", f"refs/pr/base...{pr['ref']}", "--", "core/libs/prx/*.cpp")) + return files, set(), existing + return files, exports(git("diff", "-U0", "--no-color", f"refs/pr/base...{pr['ref']}", "--", "core/libs/prx/*.cpp")), existing def related(prs, executor): mine = prs[0] scan(mine) others = {n: pr for n, pr in prs.items() if n} + base_paths = set(git("ls-tree", "-r", "--name-only", "-z", "refs/pr/base").split("\0")) chosen = {} - for index, ((n, pr), (files, found)) in enumerate(zip(others.items(), executor.map(probe, others.values())), 1): - if files & mine["files"] or found & mine["exports"]: + for index, ((n, pr), (files, found, existing)) in enumerate(zip(others.items(), executor.map(probe, others.values())), 1): + if files & mine["files"] or found & mine["exports"] or existing - base_paths: chosen[n] = pr else: pr.update(tree=None, files=set(), added=set(), exports=set()) diff --git a/tools/tests/test_pr_overlap_progress.py b/tools/tests/test_pr_overlap_progress.py index f1d263668a..80ed91007c 100644 --- a/tools/tests/test_pr_overlap_progress.py +++ b/tools/tests/test_pr_overlap_progress.py @@ -56,9 +56,10 @@ class ProgressTests(unittest.TestCase): def fake_probe(pr): number = int(pr["ref"].rsplit("/", 1)[1]) - return ({"shared.txt"} if number % 25 == 0 else {"other.txt"}), set() + return ({"shared.txt"} if number % 25 == 0 else {"other.txt"}), set(), set() - with patch.object(pr_overlap, "scan", side_effect=self.fake_scan), patch.object(pr_overlap, "probe", side_effect=fake_probe): + with patch.object(pr_overlap, "scan", side_effect=self.fake_scan), patch.object(pr_overlap, "probe", side_effect=fake_probe), \ + patch.object(pr_overlap, "git", return_value="shared.txt\0other.txt\0"): _, lines = self.capture(pr_overlap.scan_all, prs, 2) self.assertEqual(lines, [ "pr_overlap: probed 100/250 pull requests, 4 share files or exports with the branch", From b46b769188252483fb0f67160c5e183e37f41b65 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:54:22 +0000 Subject: [PATCH 179/702] test(tools): compare a pull request on a path the base renamed in pr_overlap's branch probe --- tools/tests/test_pr_overlap_branch_scan.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tools/tests/test_pr_overlap_branch_scan.py b/tools/tests/test_pr_overlap_branch_scan.py index 7ac2e8baed..d6c60ec60d 100644 --- a/tools/tests/test_pr_overlap_branch_scan.py +++ b/tools/tests/test_pr_overlap_branch_scan.py @@ -103,6 +103,27 @@ class BranchScanTests(unittest.TestCase): self.assertCountEqual(merged, ["refs/pr/1", "refs/pr/2", "refs/pr/3", "refs/pr/4"]) + def test_a_pull_request_on_a_path_the_base_renamed_is_still_compared(self): + old = run("rev-parse", "refs/pr/base") + older = commit_on_base(old, "older pull request", {"b.txt": "older\ntwo\nthree\n"}) + run("checkout", "-q", "--detach", old) + run("mv", "b.txt", "renamed.txt") + run("commit", "-q", "-m", "rename on main") + base = run("rev-parse", "HEAD") + run("update-ref", "refs/pr/base", base) + branch = commit_on_base(base, "branch", {"renamed.txt": "branch\ntwo\nthree\n"}) + run("update-ref", "refs/pr/5", older) + prs = {5: {"ref": "refs/pr/5", "sha": older, "author": "author5", "depends": set()}, + 0: {"ref": branch, "author": None, "depends": set()}} + everything = copy.deepcopy(prs) + for pr in everything.values(): + pr_overlap.scan(pr) + pr_overlap.scan_all(prs, 2) + expected = pr_overlap.overlaps(everything, 0, 5) + self.assertIsNotNone(expected) + self.assertIn("renamed.txt", expected["conflicts"]) + self.assertEqual(pr_overlap.overlaps(prs, 0, 5), expected) + if __name__ == "__main__": unittest.main() From 2229b41d832b2f219ba41ea4aeca190a2032f36f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:54:24 +0000 Subject: [PATCH 180/702] test(tools): merge a pull request on a path the base deleted in pr_overlap's branch probe --- tools/tests/test_pr_overlap_branch_scan.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tools/tests/test_pr_overlap_branch_scan.py b/tools/tests/test_pr_overlap_branch_scan.py index d6c60ec60d..a0b747ecdd 100644 --- a/tools/tests/test_pr_overlap_branch_scan.py +++ b/tools/tests/test_pr_overlap_branch_scan.py @@ -124,6 +124,28 @@ class BranchScanTests(unittest.TestCase): self.assertIn("renamed.txt", expected["conflicts"]) self.assertEqual(pr_overlap.overlaps(prs, 0, 5), expected) + def test_a_pull_request_on_a_path_the_base_deleted_is_merged(self): + old = run("rev-parse", "refs/pr/base") + older = commit_on_base(old, "older pull request", {"c.txt": "one\ntwo\nolder\n"}) + run("checkout", "-q", "--detach", old) + run("rm", "-q", "c.txt") + run("commit", "-q", "-m", "delete on main") + base = run("rev-parse", "HEAD") + run("update-ref", "refs/pr/base", base) + run("update-ref", "refs/pr/6", older) + prs = {6: {"ref": "refs/pr/6", "sha": older, "author": "author6", "depends": set()}, + 0: {"ref": commit_on_base(base, "branch", {"a.txt": "branch\ntwo\nthree\n"}), "author": None, "depends": set()}} + merged = [] + real = pr_overlap.merge + + def record(*args): + merged.append(args[1]) + return real(*args) + + with patch.object(pr_overlap, "merge", side_effect=record): + pr_overlap.scan_all(prs, 2) + self.assertIn("refs/pr/6", merged) + if __name__ == "__main__": unittest.main() From aaa0958b17c1150e865b7b19e1c899b42abe7f91 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 22:00:36 +0000 Subject: [PATCH 181/702] test(libkernel): cover sceKernelDirectMemoryQuery's block lookup, find-next and argument checks Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestMemory.cpp | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 3b28008f19..e84608f004 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -791,6 +791,42 @@ static void CheckGetDirectMemoryType() { Require(sceKernelGetDirectMemoryType(second, &type, &start, &end) == SCE_KERNEL_ERROR_ENOENT); } +static void CheckDirectMemoryQuery() { + struct QueryInfo { + std::int64_t start; + std::int64_t end; + int memoryType; + }; + constexpr std::size_t page = 0x4000; + std::int64_t first = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 5, 0, 3, &first) == 0); + Require(sceKernelReleaseDirectMemory(first + page * 2, page * 3) == 0); + std::int64_t second = 0; + Require(sceKernelAllocateDirectMemory(first + page * 4, first + page * 5, page, 0, 1, &second) == 0 && second == first + page * 4); + QueryInfo info{-1, -1, -1}; + Require(sceKernelDirectMemoryQuery(first, 0, &info, sizeof(info)) == 0); + Require(info.start == first && info.end == first + page * 2 && info.memoryType == 3); + info = {-1, -1, -1}; + Require(sceKernelDirectMemoryQuery(first + page * 2 - 1, 0, &info, sizeof(info)) == 0); + Require(info.start == first && info.end == first + page * 2 && info.memoryType == 3); + info = {-1, -1, -1}; + Require(sceKernelDirectMemoryQuery(first + page, 1, &info, sizeof(info)) == 0); + Require(info.start == first && info.end == first + page * 2 && info.memoryType == 3); + info = {-1, -1, -1}; + Require(sceKernelDirectMemoryQuery(first + page * 2, 0, &info, sizeof(info)) == SCE_KERNEL_ERROR_EACCES); + Require(info.start == -1 && info.end == -1 && info.memoryType == -1); + Require(sceKernelDirectMemoryQuery(first + page * 3, 1, &info, sizeof(info)) == 0); + Require(info.start == second && info.end == second + page && info.memoryType == 1); + info = {-1, -1, -1}; + Require(sceKernelDirectMemoryQuery(first, 0, nullptr, sizeof(info)) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelDirectMemoryQuery(-1, 1, &info, sizeof(info)) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelDirectMemoryQuery(first, 0, &info, sizeof(info) - 1) == SCE_KERNEL_ERROR_EINVAL); + Require(info.start == -1 && info.end == -1 && info.memoryType == -1); + Require(sceKernelReleaseDirectMemory(first, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(second, page) == 0); + Require(sceKernelDirectMemoryQuery(first, 0, &info, sizeof(info)) == SCE_KERNEL_ERROR_EACCES); +} + static void CheckMtypeprotect() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -3365,6 +3401,7 @@ int main(int argc, char** argv) { CheckMlock(); CheckSharedDirectMemoryLifecycle(); CheckGetDirectMemoryType(); + CheckDirectMemoryQuery(); CheckMtypeprotect(); CheckHeapAlignment(); CheckHeapAfterMappingReuse(); From c0f99624f860951581dbcedc1694c48885a58977 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 22:02:44 +0000 Subject: [PATCH 182/702] test(libkernel): cover sceKernelReadTsc's rate against sceKernelGetTscFrequency Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 7 +++++++ core/libs/tests/GuestProcessTime.cpp | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 675f341151..7f562da9ab 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -231,6 +231,13 @@ if(BUILD_TESTING) set_tests_properties(guest_process_time guest_process_time_counter PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME guest_process_time_tsc COMMAND guest_process_time_tests tsc) + add_test(NAME guest_process_time_tsc_scaled COMMAND guest_process_time_tests tsc) + set_tests_properties(guest_process_time_tsc_scaled PROPERTIES ENVIRONMENT "APS5_TIME_SCALE=0.5") + if(WIN32) + set_tests_properties(guest_process_time_tsc guest_process_time_tsc_scaled PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_process_arguments_tests ${LIBS_INCLUDE_DIR}/tests/GuestProcessArguments.cpp) target_include_directories(guest_process_arguments_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_process_arguments_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestProcessTime.cpp b/core/libs/tests/GuestProcessTime.cpp index cfff04c3bd..e9f63a46c2 100644 --- a/core/libs/tests/GuestProcessTime.cpp +++ b/core/libs/tests/GuestProcessTime.cpp @@ -1,3 +1,4 @@ +#include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include #include @@ -22,6 +23,18 @@ int main(int argc, char** argv) { const auto minimumElapsedMicros = elapsedMicros * 19 / 20; const auto slept = std::chrono::steady_clock::now() + std::chrono::microseconds(sleptMicros); while (std::chrono::steady_clock::now() < slept) std::this_thread::sleep_until(slept); + if (argc > 1 && std::strcmp(argv[1], "tsc") == 0) { + const std::uint64_t frequency = sceKernelGetTscFrequency(); + const std::uint64_t before = sceKernelReadTsc(); + const auto until = std::chrono::steady_clock::now() + std::chrono::microseconds(sleptMicros); + while (std::chrono::steady_clock::now() < until) std::this_thread::sleep_until(until); + const std::uint64_t ticks = sceKernelReadTsc() - before; + const std::uint64_t tscMicros = ticks / frequency * 1000000 + ticks % frequency * 1000000 / frequency; + Require(tscMicros >= minimumElapsedMicros, "TSC advanced less than the sleep at the TSC frequency"); + Require(tscMicros < minuteMicros, "TSC advanced more than a minute across the sleep"); + Require(sceKernelReadTsc() >= before + ticks, "TSC went backwards"); + return 0; + } if (argc > 1 && std::strcmp(argv[1], "counter") == 0) { const std::uint64_t counter = sceKernelGetProcessTimeCounter(); const std::uint64_t frequency = sceKernelGetProcessTimeCounterFrequency(); From 13069d07ee6c59431e9c3d6c763154dcdf71d801 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Mon, 26 Oct 2026 23:03:36 +0100 Subject: [PATCH 183/702] fix(libkernel_sys): link a symbolic link itself in linkat without AT_SYMLINK_FOLLOW linkat threw for a symbolic link as the source without AT_SYMLINK_FOLLOW. FreeBSD 11.4's kern_linkat looks the source up with NOFOLLOW in that case and links the symbolic link itself, also when it dangles or names a directory, since the link is not a directory. On Linux and macOS the host's linkat with flag 0 does the same, so the source is linked there after the usual destination checks (EEXIST, then ENOTDIR or ENOENT for the parent). Windows still throws. --- core/libs/prx/libkernel_sys/Export.cpp | 33 ++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 3157f7a862..dbf6f1b4ee 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -478,10 +478,16 @@ static int LinkSourceError(const std::filesystem::path& source) { return std::filesystem::is_directory(status) ? GuestEperm : 0; } +static int LinkDestinationError(const std::filesystem::path& destination) { + std::error_code error; + if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return GuestEexist; + if (!std::filesystem::is_directory(destination.parent_path(), error)) return HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent; + return 0; +} + static int LinkNative(const std::filesystem::path& source, const std::filesystem::path& destination) { + if (const int destinationError = LinkDestinationError(destination)) return static_cast(Fail(destinationError)); std::error_code error; - if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast(Fail(GuestEexist)); - if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent)); #ifdef _WIN32 std::filesystem::path target; if (const int followError = FollowLinkPath(source, target)) return static_cast(Fail(followError)); @@ -496,6 +502,18 @@ static int LinkNative(const std::filesystem::path& source, const std::filesystem return 0; } +#ifndef _WIN32 +static int LinkSymbolicLinkItself(const std::filesystem::path& source, const std::filesystem::path& destination) { + if (const int error = LinkDestinationError(destination)) return static_cast(Fail(error)); + if (::linkat(AT_FDCWD, source.c_str(), AT_FDCWD, destination.c_str(), 0) != 0) { + if (errno == EXDEV) return static_cast(Fail(GuestExdev)); + throw std::system_error(errno, std::generic_category(), "linkat"); + } + RecordWrittenPath_nid_no_patch(destination); + return 0; +} +#endif + int APS5_VABI link_nid_postfix(const char* existing, const char* created) { std::string existingCopy, createdCopy; if (const int error = CopyPath(existing, existingCopy)) return static_cast(Fail(error)); @@ -523,10 +541,17 @@ int APS5_VABI linkat_nid_postfix(int fd1, const char* path1, int fd2, const char if (const int error = CopyPath(path1, copy1)) return static_cast(Fail(error)); std::filesystem::path source, destination; if (const int error = ResolveAtPath(fd1, path1, source)) return static_cast(Fail(error)); - if (flag == 0 && IsSymbolicLink(source)) NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself"); - if (const int error = LinkSourceError(source)) return static_cast(Fail(error)); + const bool linkItself = flag == 0 && IsSymbolicLink(source); +#ifdef _WIN32 + if (linkItself) NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself on Windows"); +#endif + if (!linkItself) + if (const int error = LinkSourceError(source)) return static_cast(Fail(error)); if (const int error = CopyPath(path2, copy2)) return static_cast(Fail(error)); if (const int error = ResolveAtPath(fd2, path2, destination)) return static_cast(Fail(error)); +#ifndef _WIN32 + if (linkItself) return LinkSymbolicLinkItself(source, destination); +#endif return LinkNative(source, destination); } From bef67fc555342604b3d7b9c9376751f1b70ea3f3 Mon Sep 17 00:00:00 2001 From: hyvvs Date: Sun, 25 Oct 2026 22:12:51 +0100 Subject: [PATCH 184/702] fix(tools): detect long multiline environment reads --- tools/env_reference.py | 14 ++++++-------- tools/tests/test_env_reference.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/tools/env_reference.py b/tools/env_reference.py index d7f80595ff..1f378a9133 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -1,4 +1,5 @@ import argparse +import bisect import functools import re import sys @@ -94,15 +95,12 @@ def reads(roots, prefixes=PREFIXES): except (OSError, UnicodeDecodeError) as error: raise ValueError(f"cannot read {path.as_posix()}: {error}") from error lines = text.splitlines() - for number, line in enumerate(lines, 1): + starts = [0] + [match.end() for match in re.finditer("\n", text)] + for reader, match in line_reads(text, prefixes): + number = bisect.bisect_right(starts, match.start()) window = "\n".join(lines[number - 1:number + 2]) - if not any(prefix in window for prefix in prefixes): - continue - for reader, match in line_reads(window, prefixes): - if match.start() >= len(line): - continue - found.setdefault(match.group("name"), []).append( - (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) + found.setdefault(match.group("name"), []).append( + (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) for sites in found.values(): sites.sort() return found diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index 3b43421864..000bf5ca53 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -84,6 +84,21 @@ class EnvReferenceTests(unittest.TestCase): found = env_reference.reads([root / "core"]) self.assertIn("APS5_MULTILINE", found) + def test_long_multiline_reads_are_checked_and_keep_their_locations(self): + root = self.fixture() + source = root / "core" / "Sample.cpp" + source.write_text('auto a = std::getenv(\n\n\n\n"APS5_DEEP_STRING");\n' + 'auto b = EnvInt(\n\n\n\n"APS5_DEEP_COUNT", 17);\n', encoding="utf-8") + found = env_reference.reads([root / "core"]) + self.assertEqual(found["APS5_DEEP_STRING"], [(source.as_posix(), 1, "string", "")]) + self.assertEqual(found["APS5_DEEP_COUNT"], [(source.as_posix(), 6, "integer", "17")]) + doc = root / "ENVIRONMENT.md" + doc.write_text("", encoding="utf-8") + out = io.StringIO() + self.assertEqual(env_reference.check(found, doc, BOTH, out), 1) + self.assertIn("APS5_DEEP_STRING", out.getvalue()) + self.assertIn("APS5_DEEP_COUNT", out.getvalue()) + def test_whitespace_before_parentheses_preserves_names_kinds_and_defaults(self): root = self.fixture() source = root / "core" / "Sample.cpp" From f118a2cbaaee1b567d306486796c80820c152c66 Mon Sep 17 00:00:00 2001 From: hyvvs Date: Sun, 25 Oct 2026 22:31:52 +0100 Subject: [PATCH 185/702] fix(tools): retain context for long multiline reads --- tools/env_reference.py | 4 ++++ tools/tests/test_env_reference.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/tools/env_reference.py b/tools/env_reference.py index 1f378a9133..4b5f9800b9 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -99,6 +99,10 @@ def reads(roots, prefixes=PREFIXES): for reader, match in line_reads(text, prefixes): number = bisect.bisect_right(starts, match.start()) window = "\n".join(lines[number - 1:number + 2]) + if match.end() - starts[number - 1] > len(window): + start = max(text.rfind(token, 0, match.start()) for token in ";{}") + 1 + end = text.find(";", match.end()) + window = text[start:end + 1] if end >= 0 else text[start:] found.setdefault(match.group("name"), []).append( (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) for sites in found.values(): diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index 000bf5ca53..50729e245c 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -99,6 +99,18 @@ class EnvReferenceTests(unittest.TestCase): self.assertIn("APS5_DEEP_STRING", out.getvalue()) self.assertIn("APS5_DEEP_COUNT", out.getvalue()) + def test_long_multiline_reads_keep_surrounding_expression_context(self): + root = self.fixture() + source = root / "core" / "Sample.cpp" + source.write_text('auto a = std::getenv(\n\n\n\n"APS5_DEEP_PRESENT") != nullptr;\n' + 'auto b = atoi(\n\n\n\nstd::getenv(\n\n\n\n"APS5_DEEP_INTEGER"));\n' + 'auto c = std::getenv(\n\n\n\n"APS5_DEEP_DEFAULT") ? 0 : 99;\n', + encoding="utf-8") + found = env_reference.reads([root / "core"]) + self.assertEqual(found["APS5_DEEP_PRESENT"], [(source.as_posix(), 1, "presence", "")]) + self.assertEqual(found["APS5_DEEP_INTEGER"], [(source.as_posix(), 10, "integer", "")]) + self.assertEqual(found["APS5_DEEP_DEFAULT"], [(source.as_posix(), 15, "string", "99")]) + def test_whitespace_before_parentheses_preserves_names_kinds_and_defaults(self): root = self.fixture() source = root / "core" / "Sample.cpp" From 927206f854398cd10cc6a450dfa309965dacece3 Mon Sep 17 00:00:00 2001 From: hyvvs Date: Sun, 25 Oct 2026 22:38:20 +0100 Subject: [PATCH 186/702] fix(tools): classify environment reads by statement --- tools/env_reference.py | 9 +++------ tools/tests/test_env_reference.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/tools/env_reference.py b/tools/env_reference.py index 4b5f9800b9..7da9d1fab1 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -94,15 +94,12 @@ def reads(roots, prefixes=PREFIXES): text = path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError) as error: raise ValueError(f"cannot read {path.as_posix()}: {error}") from error - lines = text.splitlines() starts = [0] + [match.end() for match in re.finditer("\n", text)] for reader, match in line_reads(text, prefixes): number = bisect.bisect_right(starts, match.start()) - window = "\n".join(lines[number - 1:number + 2]) - if match.end() - starts[number - 1] > len(window): - start = max(text.rfind(token, 0, match.start()) for token in ";{}") + 1 - end = text.find(";", match.end()) - window = text[start:end + 1] if end >= 0 else text[start:] + start = max(text.rfind(token, 0, match.start()) for token in ";{}") + 1 + end = text.find(";", match.end()) + window = text[start:end + 1] if end >= 0 else text[start:] found.setdefault(match.group("name"), []).append( (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) for sites in found.values(): diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index 50729e245c..440cddbc9b 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -111,6 +111,21 @@ class EnvReferenceTests(unittest.TestCase): self.assertEqual(found["APS5_DEEP_INTEGER"], [(source.as_posix(), 10, "integer", "")]) self.assertEqual(found["APS5_DEEP_DEFAULT"], [(source.as_posix(), 15, "string", "99")]) + def test_short_reads_in_long_expressions_keep_their_kind_and_default(self): + root = self.fixture() + source = root / "core" / "Sample.cpp" + source.write_text('auto integer = atoi(\n\n\n\nstd::getenv("APS5_SHORT_INTEGER"));\n' + 'auto present = std::getenv("APS5_SHORT_PRESENT")\n\n\n\n!= nullptr;\n' + 'auto text = std::getenv("APS5_STILL_STRING");\n' + 'if (text != nullptr) text = nullptr;\n' + 'auto fallback = std::getenv("APS5_SHORT_DEFAULT")\n\n\n\n? 1 : 42;\n', + encoding="utf-8") + found = env_reference.reads([root / "core"]) + self.assertEqual(found["APS5_SHORT_INTEGER"], [(source.as_posix(), 5, "integer", "")]) + self.assertEqual(found["APS5_SHORT_PRESENT"], [(source.as_posix(), 6, "presence", "")]) + self.assertEqual(found["APS5_STILL_STRING"], [(source.as_posix(), 11, "string", "")]) + self.assertEqual(found["APS5_SHORT_DEFAULT"], [(source.as_posix(), 13, "string", "42")]) + def test_whitespace_before_parentheses_preserves_names_kinds_and_defaults(self): root = self.fixture() source = root / "core" / "Sample.cpp" From 3e008e91cf94824badf2aab3ebdc2865135de4e8 Mon Sep 17 00:00:00 2001 From: hyvvs Date: Sun, 25 Oct 2026 23:00:30 +0100 Subject: [PATCH 187/702] fix(tools): ignore non-code punctuation in environment reads --- tools/env_reference.py | 13 ++++++++++--- tools/tests/test_env_reference.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/tools/env_reference.py b/tools/env_reference.py index 7da9d1fab1..7d250577a7 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -5,6 +5,9 @@ import re import sys from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from nid_names import CPP_TOKEN + PREFIXES = ("APS5_", "ANYPS5_") DOC = "docs/dev/ENVIRONMENT.md" ROOTS = ("core",) @@ -94,11 +97,15 @@ def reads(roots, prefixes=PREFIXES): text = path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError) as error: raise ValueError(f"cannot read {path.as_posix()}: {error}") from error + matches = line_reads(text, prefixes) + if not matches: + continue + code = CPP_TOKEN.sub(lambda token: token.group() if token.group("code") else " " * len(token.group()), text) starts = [0] + [match.end() for match in re.finditer("\n", text)] - for reader, match in line_reads(text, prefixes): + for reader, match in matches: number = bisect.bisect_right(starts, match.start()) - start = max(text.rfind(token, 0, match.start()) for token in ";{}") + 1 - end = text.find(";", match.end()) + start = max(code.rfind(token, 0, match.start()) for token in ";{}") + 1 + end = code.find(";", match.end()) window = text[start:end + 1] if end >= 0 else text[start:] found.setdefault(match.group("name"), []).append( (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index 440cddbc9b..8a1de3df87 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -126,6 +126,22 @@ class EnvReferenceTests(unittest.TestCase): self.assertEqual(found["APS5_STILL_STRING"], [(source.as_posix(), 11, "string", "")]) self.assertEqual(found["APS5_SHORT_DEFAULT"], [(source.as_posix(), 13, "string", "42")]) + def test_statement_boundaries_ignore_comments_and_literals(self): + cases = ( + ('auto value = atoi(/* ;{} */ std::getenv("APS5_VALUE"));', "integer", ""), + ('auto value = std::getenv("APS5_VALUE") /* ;{} */ != nullptr;', "presence", ""), + ("auto value = std::getenv(\"APS5_VALUE\") ? ';' : 99;", "string", "99"), + ('auto value = std::atoi((std::puts(R"tag(;{})tag"), std::getenv("APS5_VALUE")));', "integer", ""), + ('auto value = std::atoi((std::puts(";{}"), std::getenv("APS5_VALUE")));', "integer", ""), + ) + root = self.fixture() + source = root / "core" / "Sample.cpp" + for text, kind, default in cases: + with self.subTest(source=text): + source.write_text(text + "\n", encoding="utf-8") + found = env_reference.reads([root / "core"]) + self.assertEqual(found["APS5_VALUE"], [(source.as_posix(), 1, kind, default)]) + def test_whitespace_before_parentheses_preserves_names_kinds_and_defaults(self): root = self.fixture() source = root / "core" / "Sample.cpp" From 3507a7ae46b7ecdd8734906805c477ecce7aea80 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Mon, 26 Oct 2026 23:05:04 +0100 Subject: [PATCH 188/702] test(libkernel_sys): check that linkat links a symbolic link itself without AT_SYMLINK_FOLLOW On Linux and macOS, linkat with flag 0 gives a symbolic link, a dangling one and one naming a directory a second name: the new name reads the same target, the link's own count is 2 and the target's is unchanged. With AT_SYMLINK_FOLLOW the dangling link fails with ENOENT and the directory link with EPERM. A taken destination fails with EEXIST, a missing parent with ENOENT and a file parent with ENOTDIR. Windows still throws. --- core/libs/tests/GuestKernelSys.cpp | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 126dce19e7..1979fe5248 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -33,6 +33,7 @@ #ifndef _WIN32 #include #include +#include #include #endif @@ -67,6 +68,14 @@ bool ThrowsNaming(Call call, const char* text) { return false; } +#ifndef _WIN32 +nlink_t LinkCount(const std::string& path) { + struct stat status {}; + Require(::lstat(path.c_str(), &status) == 0); + return status.st_nlink; +} +#endif + void OwnershipAndLinkTests(const std::filesystem::path& root) { constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; const auto file = (root / "owned.txt").string(); @@ -137,7 +146,31 @@ void OwnershipAndLinkTests(const std::filesystem::path& root) { Require(lchown_nid_postfix(dangling.c_str(), 0, 0) == 0); Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0) == -1 && Error() == 2); Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0x200) == 0); +#ifdef _WIN32 Require(ThrowsNaming([&] { linkat_nid_postfix(-100, symbolic.c_str(), -100, other.c_str(), 0); }, "symbolic link itself")); +#else + const auto symbolicCopy = (root / "copy.link").string(); + const auto danglingCopy = (root / "dangling.copy").string(); + Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, symbolicCopy.c_str(), 0) == 0); + Require(std::filesystem::read_symlink(symbolicCopy) == "owned.txt" && LinkCount(symbolic) == 2); + Require(std::filesystem::hard_link_count(file) == 2); + Require(linkat_nid_postfix(directory, "dangling.link", directory, "dangling.copy", 0) == 0); + Require(std::filesystem::read_symlink(danglingCopy) == "missing.txt" && LinkCount(dangling) == 2); + Require(linkat_nid_postfix(-100, dangling.c_str(), -100, other.c_str(), 0x400) == -1 && Error() == 2); + Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, linked.c_str(), 0) == -1 && Error() == 17); + Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, (missing + "/inner").c_str(), 0) == -1 && Error() == 2); + Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, (throughFile + "/inner").c_str(), 0) == -1 && Error() == 20); + Require(LinkCount(symbolic) == 2 && !std::filesystem::exists(std::filesystem::symlink_status(other))); + const auto folder = root / "folder"; + const auto folderLink = (root / "folder.link").string(); + const auto folderCopy = (root / "folder.copy").string(); + Require(std::filesystem::create_directory(folder) && symlink_nid_postfix("folder", folderLink.c_str()) == 0); + Require(linkat_nid_postfix(-100, folderLink.c_str(), -100, other.c_str(), 0x400) == -1 && Error() == 1); + Require(linkat_nid_postfix(-100, folderLink.c_str(), -100, folderCopy.c_str(), 0) == 0); + Require(std::filesystem::read_symlink(folderCopy) == "folder" && LinkCount(folderLink) == 2); + Require(std::filesystem::remove(symbolicCopy) && std::filesystem::remove(danglingCopy) && std::filesystem::remove(folderCopy)); + Require(std::filesystem::remove(folderLink) && std::filesystem::remove(folder)); +#endif Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, other.c_str(), 0x400) == 0); #ifdef _WIN32 Require((GetFileAttributesW(std::filesystem::path(other).c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) == 0); From 0f2a263a20185d62257afe6313593e330a28b8af Mon Sep 17 00:00:00 2001 From: Alvaro Date: Mon, 26 Oct 2026 23:05:05 +0100 Subject: [PATCH 189/702] docs(libkernel_sys): narrow the linkat symbolic link debt to Windows --- docs/dev/debt/libkernel_sys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 6987cb2840..911a84f29e 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -31,7 +31,7 @@ - [Console identity](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - `get_authinfo` errors and capability bits remain unverified on PS5; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp), leaving the authority ID and other capability, attribute and credential words zero. The host models a retail unit (`IsCEX` 1, `IsDevKit` 0). The system OpenPSID reuses the ordinary ID; firmware 13.42 reads a separate `machdep.openpsid_for_sys`, whose value is unknown. Null returns positive 22 per the [module report](https://github.com/boykopovar/AnyPS5/pull/10524#issuecomment-6393566914). - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [chown, lchown and fchownat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled, as for `fchown`: owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws, naming it. The path is looked up first, as in FreeBSD 11's `kern_fchownat`, with `stat`, with `lstat` for `lchown`, and with `fstatat` for `fchownat`, so a lookup error is returned before the owner is checked and nothing is changed on the host. `fchownat` flags other than `AT_SYMLINK_NOFOLLOW` (0x200) return `EINVAL`, as in FreeBSD 11. The console's ownership rules are unverified -- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) resolves `AT_FDCWD` (-100) and absolute paths as `link` does, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. 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. A symbolic link as the source without `AT_SYMLINK_FOLLOW` throws, where FreeBSD links the symbolic link itself. Console behaviour is unverified +- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) resolves `AT_FDCWD` (-100) and absolute paths as `link` does, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. 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. A symbolic link as the source without `AT_SYMLINK_FOLLOW` throws on Windows, where FreeBSD links the symbolic link itself; on Linux and macOS the host's `linkat` with flag 0 links it. Console behaviour is unverified - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 - [sceKernelTitleWorkaroundIsEnabled](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; the `{version, align, ids[2]}` layout, the bit test and the limit of 0x3A bits follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/kernel.cpp), which notes that the limit changes between firmwares. A negative bit is `EINVAL` here; shadPS4 compares it signed and reads outside `ids`. Titles usually pass the `title_workaround` block of `sceKernelGetAppInfo`, which does not write that block here, so the bits read are whatever the caller's memory held - [sceKernelGetProcessType](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; as in shadPS4, the title's own pid, -1 and 0 are type 0 (big application) and any other pid is `ESRCH` From 077bbfc98d867f8359b68f97e98280147804a2be Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 22:10:30 +0000 Subject: [PATCH 190/702] test(libkernel): cover the APR prefixed resolve, file size and stat queries and the submission with a result Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 1 + core/libs/tests/GuestAprRead.cpp | 41 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index c4f1acc4bc..941e1b916f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -116,6 +116,7 @@ if(BUILD_TESTING) if(WIN32) list(APPEND aprReadCases fault stack) endif() + list(APPEND aprReadCases queries) foreach(aprReadCase IN LISTS aprReadCases) set(aprReadRun ${CMAKE_CURRENT_BINARY_DIR}/apr_read_${aprReadCase}) file(MAKE_DIRECTORY ${aprReadRun}) diff --git a/core/libs/tests/GuestAprRead.cpp b/core/libs/tests/GuestAprRead.cpp index 004346dbcd..da7ee7df7c 100644 --- a/core/libs/tests/GuestAprRead.cpp +++ b/core/libs/tests/GuestAprRead.cpp @@ -173,6 +173,45 @@ void TestStreams(const Fixture& fixture) { Require(held, "cannot hold 600 guest fopen streams"); } +void TestQueries() { + constexpr int GuestEinval = 22; + constexpr int GuestEnoent = 2; + const char* names[] = {"generated.bin", "missing.bin"}; + std::array ids{0, 0}; + std::array sizes{1, 1}; + std::uint32_t failed = 7; + Require(sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes("apr_read_", names, 1, ids.data(), sizes.data(), &failed) == 0, "APR did not resolve a prefixed path"); + Require(failed == 7 && sizes[0] == FileBytes, "APR did not report the prefixed file's size"); + const auto id = ids[0]; + ids = {0, 0}; + sizes = {1, 1}; + Require(sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes("apr_read_", names, 2, ids.data(), sizes.data(), &failed) == -1 && *__error_nid_postfix() == GuestEnoent, "APR resolved a missing prefixed path"); + Require(failed == 1 && ids[0] == id && sizes[0] == FileBytes && ids[1] == 0xFFFFFFFFu && sizes[1] == 0, "APR did not mark only the missing prefixed path"); + std::uint64_t size = 0; + Require(sceKernelAprGetFileSize(id, nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "APR accepted a null file size"); + Require(sceKernelAprGetFileSize(id, &size) == 0 && size == FileBytes, "APR reported a wrong file size"); + FileStat stat{}; + Require(sceKernelAprGetFileStat(id, nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "APR accepted a null file stat"); + Require(sceKernelAprGetFileStat(id, &stat) == 0 && stat.st_size == static_cast(FileBytes), "APR reported a wrong file stat size"); + std::vector data(4096, 0xCC); + const Apr::ReadFileCommand command{{Apr::Opcode::ReadFile, sizeof(Apr::ReadFileCommand)}, id, 0, reinterpret_cast(data.data()), data.size(), 100}; + alignas(8) std::array memory{}; + std::memcpy(memory.data(), &command, sizeof(command)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 1, sizeof(memory), memory.data()}; + std::uint32_t result = 0xFFFFFFFFu; + std::uint32_t first = 0; + Require(sceKernelAprSubmitCommandBufferAndGetResult(&buffer, 0, nullptr, &first) == -1 && *__error_nid_postfix() == GuestEinval && first == 0, "APR accepted a null result"); + Require(sceKernelAprSubmitCommandBufferAndGetResult(nullptr, 0, &result, &first) == -1 && *__error_nid_postfix() == GuestEinval, "APR accepted a null command buffer"); + Require(sceKernelAprSubmitCommandBufferAndGetResult(&buffer, 0, &result, nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "APR accepted a null submission id"); + Require(result == 0xFFFFFFFFu && first == 0, "a rejected APR submission wrote its result or id"); + Require(sceKernelAprSubmitCommandBufferAndGetResult(&buffer, 0, &result, &first) == 0 && result == 0 && first != 0, "APR did not return a result and an id"); + Require(sceKernelAprWaitCommandBuffer(first) == 0, "APR did not wait for the submission"); + for (std::size_t index = 0; index < data.size(); ++index) Require(data[index] == FileByte(100 + index), "APR read through the submission with a result returned incorrect bytes"); + std::uint32_t second = 0; + Require(sceKernelAprSubmitCommandBufferAndGetResult(&buffer, 1, &result, &second) == 0 && second == first + 1, "APR submission ids did not count up"); + Require(sceKernelAprWaitCommandBuffer(second) == 0, "APR did not wait for the second submission"); +} + #ifdef _WIN32 std::uintptr_t faultBegin = 0; std::size_t faultBytes = 0; @@ -263,6 +302,8 @@ int main(int argc, char** argv) { TestRead(fixture, storage.Data()); } else if (mode == "eof") { TestEof(fixture); + } else if (mode == "queries") { + TestQueries(); } else if (mode == "errors") { TestErrors(fixture); } else if (mode == "streams") { From 2ce1e917e2b0c20221e83bc16039faeda259d3b4 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 22:17:45 +0000 Subject: [PATCH 191/702] test(agc): cover the end-of-pipe action data and GCR control patches Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/tests/Commands.cpp | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index a9ec29ed76..5b30b085e1 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include "prx/libSceAgc/Patch/include/WaitRegMem.hpp" #include "prx/libSceAgc/Patch/include/Nop.hpp" +#include "prx/libSceAgc/Patch/include/QueueEndOfPipeAction.hpp" #include "prx/libSceAgc/Misc/include/Suspend.hpp" #include "prx/libSceAgc/Misc/include/RegisterDefaults.hpp" #include "prx/libSceAgc/Misc/include/PacketInfo.hpp" @@ -555,6 +556,32 @@ void testMemory() { check(storage.words == beforeMask, "invalid mask patch modified packet memory"); } +void testEndOfPipePatches() { + std::array packet{}; + packet[0] = Agc::Command::Header(0x49u, 8); + packet[1] = 0xf0000fffu; + packet[2] = 2u << 29u; + check(sceAgcQueueEndOfPipeActionPatchData(packet.data(), 0x1122334455667788ull) == 0, "end-of-pipe data patch failed"); + check(packet[5] == 0x55667788u && packet[6] == 0x11223344u, "64-bit end-of-pipe data patch did not write both halves"); + packet[2] = 1u << 29u; + check(sceAgcQueueEndOfPipeActionPatchData(packet.data(), 0x0badf00du) == 0, "32-bit end-of-pipe data patch failed"); + check(packet[5] == 0x0badf00du && packet[6] == 0u, "32-bit end-of-pipe data patch wrote the wrong words"); + expectFailure([&] { sceAgcQueueEndOfPipeActionPatchData(packet.data(), 0x100000000ull); }); + check(packet[5] == 0x0badf00du && packet[6] == 0u, "a rejected 32-bit data patch changed the packet"); + check(sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 0x0a5u) == 0, "end-of-pipe GCR control patch failed"); + check(packet[1] == (0xf0000fffu | (0x0a5u << 12u)), "end-of-pipe GCR control patch did not keep the other control bits"); + check(sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 0x100u) == 0 && packet[1] == (0xf0000fffu | (0x300u << 12u)), "end-of-pipe GCR control 0x100 did not gain 0x200"); + check(sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 0x200u) == 0 && packet[1] == (0xf0000fffu | (0x200u << 12u)), "end-of-pipe GCR control 0x200 changed"); + const auto before = packet; + expectFailure([&] { sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 0x1000u); }); + check(packet == before, "a rejected GCR control patch changed the packet"); + packet[0] = Agc::Command::Header(0x47u, 6); + expectFailure([&] { sceAgcQueueEndOfPipeActionPatchData(packet.data(), 1); }); + expectFailure([&] { sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 1); }); + packet[0] = before[0]; + check(packet == before, "a data or GCR control patch of another packet changed it"); +} + struct AtomicGdsAllocation { Storage storage; std::uint32_t capacity = 15; @@ -830,6 +857,7 @@ int main(int argc, char** argv) { testRegisterRange(); testPacketPayloadAddress(); testMemory(); + testEndOfPipePatches(); testAtomicGds(); testDebugRaiseException(); testDefaults(); From cbbae0c2216ac52ce65dbd81566d082ed0de7dc5 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 22:21:38 +0000 Subject: [PATCH 192/702] test(libkernel): bound the TSC interval by the measured time at the time scale Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestProcessTime.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestProcessTime.cpp b/core/libs/tests/GuestProcessTime.cpp index e9f63a46c2..0d75b8f43f 100644 --- a/core/libs/tests/GuestProcessTime.cpp +++ b/core/libs/tests/GuestProcessTime.cpp @@ -25,13 +25,15 @@ int main(int argc, char** argv) { while (std::chrono::steady_clock::now() < slept) std::this_thread::sleep_until(slept); if (argc > 1 && std::strcmp(argv[1], "tsc") == 0) { const std::uint64_t frequency = sceKernelGetTscFrequency(); + const auto start = std::chrono::steady_clock::now(); const std::uint64_t before = sceKernelReadTsc(); - const auto until = std::chrono::steady_clock::now() + std::chrono::microseconds(sleptMicros); + const auto until = start + std::chrono::microseconds(sleptMicros); while (std::chrono::steady_clock::now() < until) std::this_thread::sleep_until(until); const std::uint64_t ticks = sceKernelReadTsc() - before; + const auto measuredMicros = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); const std::uint64_t tscMicros = ticks / frequency * 1000000 + ticks % frequency * 1000000 / frequency; Require(tscMicros >= minimumElapsedMicros, "TSC advanced less than the sleep at the TSC frequency"); - Require(tscMicros < minuteMicros, "TSC advanced more than a minute across the sleep"); + Require(static_cast(tscMicros) <= static_cast(measuredMicros) * (scale > 0.0 ? scale : 1.0) * 1.05, "TSC advanced more than the measured time at the time scale"); Require(sceKernelReadTsc() >= before + ticks, "TSC went backwards"); return 0; } From f38b6bc2883d722e556b79741a5e0250dbdf6997 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:38:40 +0300 Subject: [PATCH 193/702] fix(agc): unify descriptor pendingWrite policy and enforce invalidation epochs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom / Log: - 26102026_05_33_48.log:L3022-L3034: [gpu] driver failure: compute shader 0x248a05b00: runtime image descriptor is invalid Root Cause: - core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp:ShaderMemory::read:L302-L361 and ShaderMemory::readWords:L441-L463 — per-dword PendingWrite decisions on wordwise pages allowed leading dwords of a 32-byte T# to be cached with PendingWrite::Raw before a later dword waited on the GPU, and page->valid.All() in readWords() copied wordwise pages without re-checking descriptorSpanBytes or pendingWrite policy. Change: - Propagate the remaining descriptor span size through SrtRuntime::descriptorScope during SRT tape and evaluator reads, restrict the page->valid.All() fast-path in readWords() to non-wordwise pages, re-evaluate cached wordwise words against checkSpanBytes, invalidate cached Raw words when a read waits on the GPU, and require a stable rawInvalidations epoch across up to 3 attempts (forcing PendingWrite::Sync on the final attempt and throwing if the epoch fails to stabilize). Metric / Verification: - ShaderMemory and SrtWalker reads never mix pre-wait Raw words with post-wait words across a descriptor span and refuse unstable snapshots. --- .../Execution/include/ShaderMemory.hpp | 10 + .../Execution/src/ShaderMemory.cpp | 192 ++++++++++++++---- .../include/Optimization/SrtWalker.hpp | 18 ++ .../Optimization/SrtWalker/SrtEvaluator.hpp | 3 + .../src/SrtWalker/SrtDescriptorEvaluation.cpp | 41 +++- .../src/SrtWalker/SrtEvaluator.cpp | 35 +++- .../Optimization/src/SrtWalker/SrtTape.cpp | 32 ++- 7 files changed, 274 insertions(+), 57 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp index dacb784275..a03ede14b3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp @@ -100,6 +100,10 @@ private: return (bits[index / 64u] & block) == block; } void Set(std::size_t index) { bits[index / 64u] |= std::uint64_t{1} << (index % 64u); } + void Clear(std::size_t index) { bits[index / 64u] &= ~(std::uint64_t{1} << (index % 64u)); } + void ClearMask(const WordMask& other) { + for (std::size_t i = 0; i < bits.size(); ++i) bits[i] &= ~other.bits[i]; + } void SetAll() { bits.fill(~std::uint64_t{0}); } void SetBlock(std::size_t index) { bits[index / 64u] |= ((std::uint64_t{1} << BlockWords) - 1u) << (index % 64u / BlockWords * BlockWords); } void Reset() { bits.fill(0u); } @@ -117,6 +121,7 @@ private: struct Page { std::array words; WordMask valid; + WordMask raw; WordMask read; WordMask recent; bool wordwise = false; @@ -137,6 +142,8 @@ private: static ShaderRecompiler::SrtWriteStamp stampWrites(void* context, std::uint64_t address, std::uint64_t bytes); static bool unchangedSince(void* context, std::uint64_t address, std::uint64_t bytes, const ShaderRecompiler::SrtWriteStamp& stamp); static bool supplyWords(void* context, const std::shared_ptr& words); + static std::uint64_t descriptorScope(void* context, std::size_t bytes); + bool invalidateRawPages(); Page& page(std::uint64_t base); [[nodiscard]] std::size_t pageAtOrAfter(std::uint64_t base) const; Page& addPage(std::size_t position, std::uint64_t base); @@ -158,6 +165,9 @@ private: PendingWriteObserver observe = nullptr; HookWaitCounter hookWaits = nullptr; PendingWriteQuery pendingSpan = nullptr; + std::size_t descriptorSpanBytes = 0; + std::uint64_t rawInvalidations = 0; + std::uint32_t forceSync = 0; }; // The positions, among a dispatch-cache variant's stored words, of the pure flat-SRT leaves a diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index 4fc0fb89bc..2be5b38675 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -235,6 +235,7 @@ ShaderMemory::Page& ShaderMemory::addPage(std::size_t position, std::uint64_t ba pool.pop_back(); } owned->valid.Reset(); + owned->raw.Reset(); owned->read.Reset(); owned->recent.Reset(); owned->wordwise = false; @@ -299,12 +300,38 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val GuestMemory::CopyGuestBytes(&page.words[first], reinterpret_cast((address & ~static_cast(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t)); page.valid.SetBlock(index); } + const auto spanNeedsSync = [](PendingWrite spanPolicy) { + return spanPolicy == PendingWrite::Sync || spanPolicy == PendingWrite::VerifyRaw || spanPolicy == PendingWrite::VerifyKnownValue; + }; + const auto spanInvalidatesCache = [&](PendingWrite spanPolicy) { + return spanNeedsSync(spanPolicy) || spanPolicy == PendingWrite::KnownValue; + }; + const auto checkSpanBytes = std::max(self.descriptorSpanBytes, sizeof(*value)); + if (page.wordwise && page.valid.Test(index) && + (self.forceSync != 0u ? self.pendingWrite(address, checkSpanBytes, {}) != PendingWrite::None + : (spanInvalidatesCache(self.pendingWrite(address, checkSpanBytes, {})) || + (checkSpanBytes > sizeof(*value) && spanInvalidatesCache(self.pendingWrite(address, sizeof(*value), {})))))) { + page.valid.Clear(index); + page.raw.Clear(index); + } if (!page.valid.Test(index)) { std::uint32_t word = 0; auto policy = PendingWrite::Sync; + bool forcedRaw = false; if (page.wordwise) { - policy = self.pendingWrite(address, sizeof(word), std::as_writable_bytes(std::span(&word, 1))); - if (policy != PendingWrite::None && policy != PendingWrite::Sync && !GuestMemory::Accessible(reinterpret_cast(address), sizeof(word))) policy = PendingWrite::Sync; + if (self.descriptorSpanBytes > sizeof(word) && + (self.forceSync != 0u ? self.pendingWrite(address, self.descriptorSpanBytes, {}) != PendingWrite::None + : spanNeedsSync(self.pendingWrite(address, self.descriptorSpanBytes, {})))) { + policy = PendingWrite::Sync; + forcedRaw = self.forceSync != 0u && !spanNeedsSync(self.pendingWrite(address, self.descriptorSpanBytes, {})); + } else { + policy = self.pendingWrite(address, sizeof(word), std::as_writable_bytes(std::span(&word, 1))); + if (self.forceSync != 0u && policy != PendingWrite::None) { + forcedRaw = !spanNeedsSync(policy); + policy = PendingWrite::Sync; + } + if (policy != PendingWrite::None && policy != PendingWrite::Sync && !GuestMemory::Accessible(reinterpret_cast(address), sizeof(word))) policy = PendingWrite::Sync; + } } // A read is evidence only when the hook waited for unfinished GPU work across it. const auto hookWaits = [&] { return self.hookWaits != nullptr ? self.hookWaits() : 0; }; @@ -313,38 +340,44 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val else ++CaptureTotals().observationsSkipped; }; const auto fetchStarted = CaptureProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + bool waited = false; if (policy == PendingWrite::KnownValue || policy == PendingWrite::VerifyKnownValue) { // The query stored the known dword in `word`. ++CaptureTotals().wordsKnown; if (policy == PendingWrite::VerifyKnownValue) { - std::uint32_t waited = 0; + std::uint32_t verifiedWord = 0; const auto waitsBefore = hookWaits(); - GuestMemory::Read(address, std::as_writable_bytes(std::span(&waited, 1)), alignof(std::uint32_t)); + GuestMemory::Read(address, std::as_writable_bytes(std::span(&verifiedWord, 1)), alignof(std::uint32_t)); ++CaptureTotals().wordsKnownVerified; - if (waited != word) ++CaptureTotals().wordsKnownMismatches; - if (self.observe != nullptr) report(waitsBefore, waited == word); - word = waited; + if (verifiedWord != word) ++CaptureTotals().wordsKnownMismatches; + if (self.observe != nullptr) report(waitsBefore, verifiedWord == word); + waited = page.wordwise && (self.hookWaits != nullptr ? self.hookWaits() != waitsBefore : true); + word = verifiedWord; } } else if (policy == PendingWrite::Raw || policy == PendingWrite::VerifyRaw) { GuestMemory::GuardRead(address, sizeof(word)); std::memcpy(&word, reinterpret_cast(address), sizeof(word)); ++CaptureTotals().wordsRaw; if (policy == PendingWrite::VerifyRaw) { - std::uint32_t waited = 0; + std::uint32_t verifiedWord = 0; const auto waitsBefore = hookWaits(); - GuestMemory::Read(address, std::as_writable_bytes(std::span(&waited, 1)), alignof(std::uint32_t)); + GuestMemory::Read(address, std::as_writable_bytes(std::span(&verifiedWord, 1)), alignof(std::uint32_t)); ++CaptureTotals().wordsVerified; - if (waited != word) ++CaptureTotals().wordMismatches; - if (self.observe != nullptr) report(waitsBefore, waited == word); - word = waited; + if (verifiedWord != word) ++CaptureTotals().wordMismatches; + if (self.observe != nullptr) report(waitsBefore, verifiedWord == word); + waited = page.wordwise && (self.hookWaits != nullptr ? self.hookWaits() != waitsBefore : true); + word = verifiedWord; } } else { // On a word-wise page the bytes before the hook's wait are kept for the observer. const bool observed = page.wordwise && policy == PendingWrite::Sync && self.observe != nullptr && GuestMemory::Accessible(reinterpret_cast(address), sizeof(word)); std::uint32_t before = 0; if (observed) std::memcpy(&before, reinterpret_cast(address), sizeof(before)); - const auto waitsBefore = observed ? hookWaits() : 0; + const auto waitsBefore = page.wordwise ? hookWaits() : 0; const auto started = page.wordwise ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + if (page.wordwise && policy == PendingWrite::Sync && self.descriptorSpanBytes > sizeof(word)) { + GuestMemory::FlushGpuWrites(address, self.descriptorSpanBytes); + } GuestMemory::Read(address, std::as_writable_bytes(std::span(&word, 1)), alignof(std::uint32_t)); if (page.wordwise) { const auto nanoseconds = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()); @@ -352,12 +385,16 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val ++CaptureTotals().wordWaits; CaptureTotals().wordWaitNanoseconds += nanoseconds; } + waited = policy != PendingWrite::None && (self.hookWaits != nullptr ? self.hookWaits() != waitsBefore : true); } if (observed) report(waitsBefore, before == word); } + if ((waited || (page.wordwise && policy == PendingWrite::Sync && !forcedRaw)) && self.invalidateRawPages()) ++self.rawInvalidations; if (CaptureProfiled()) CaptureTotals().wordReadNanoseconds += NanosecondsSince(fetchStarted); page.words[index] = word; page.valid.Set(index); + if (page.wordwise && (policy == PendingWrite::Raw || policy == PendingWrite::KnownValue || (forcedRaw && !waited))) page.raw.Set(index); + else page.raw.Clear(index); } page.read.Set(index); page.recent.Set(index); @@ -365,6 +402,17 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val return true; } +bool ShaderMemory::invalidateRawPages() { + bool invalidated = false; + for (auto& [pageBase, existing] : pages) { + if (!existing->wordwise || existing->raw.None()) continue; + existing->valid.ClearMask(existing->raw); + existing->raw.Reset(); + invalidated = true; + } + return invalidated; +} + bool ShaderMemory::overlapsInitial(std::uint64_t begin, std::uint64_t end) const { if (initial.empty()) return false; const auto after = std::lower_bound(initial.begin(), initial.end(), begin, [](const auto& entry, std::uint64_t value) { return entry.first < value; }); @@ -375,43 +423,96 @@ bool ShaderMemory::overlapsInitial(std::uint64_t begin, std::uint64_t end) const bool ShaderMemory::readWords(void* context, std::uint64_t address, std::uint32_t* values, std::uint64_t count) { auto& self = *static_cast(context); if (!self.supplied.empty() && count != 0) self.spillTouching(address, count > (std::numeric_limits::max() - address) / sizeof(std::uint32_t) ? std::numeric_limits::max() : address + count * sizeof(std::uint32_t)); - while (count != 0) { - const auto base = address & ~static_cast(PageBytes - 1); - const auto index = static_cast((address % PageBytes) / sizeof(std::uint32_t)); - const auto words = static_cast(std::min(count, PageWords - index)); - const auto bytes = static_cast(words) * sizeof(std::uint32_t); - Page* page = nullptr; - if (address % sizeof(std::uint32_t) == 0 && address <= std::numeric_limits::max() - bytes && !self.overlapsInitial(address, address + bytes)) page = &self.page(base); - if (page != nullptr && page->lazy) { - const auto end = (index + words + BlockWords - 1u) / BlockWords * BlockWords; - auto first = index / BlockWords * BlockWords; - while (first < end) { - if (page->valid.TestBlock(first)) { - first += BlockWords; - continue; + const auto startAddress = address; + auto* const startValues = values; + const auto startCount = count; + const auto savedSpanBytes = self.descriptorSpanBytes; + const auto savedForceSync = self.forceSync; + struct ScopeReset { + ShaderMemory& self; + std::size_t spanBytes; + std::uint32_t forceSync; + ~ScopeReset() { + self.descriptorSpanBytes = spanBytes; + self.forceSync = forceSync; + } + } scopeReset{self, savedSpanBytes, savedForceSync}; + const auto strideWords = savedSpanBytes / sizeof(std::uint32_t); + for (std::uint32_t attempt = 0; attempt < 3u; ++attempt) { + if (attempt == 2u) ++self.forceSync; + address = startAddress; + values = startValues; + count = startCount; + const auto invalidationsBefore = self.rawInvalidations; + while (count != 0) { + const auto base = address & ~static_cast(PageBytes - 1); + const auto index = static_cast((address % PageBytes) / sizeof(std::uint32_t)); + const auto words = static_cast(std::min(count, PageWords - index)); + const auto bytes = static_cast(words) * sizeof(std::uint32_t); + Page* page = nullptr; + if (address % sizeof(std::uint32_t) == 0 && address <= std::numeric_limits::max() - bytes && !self.overlapsInitial(address, address + bytes)) page = &self.page(base); + if (page != nullptr && page->lazy) { + const auto end = (index + words + BlockWords - 1u) / BlockWords * BlockWords; + auto first = index / BlockWords * BlockWords; + while (first < end) { + if (page->valid.TestBlock(first)) { + first += BlockWords; + continue; + } + auto last = first + BlockWords; + while (last < end && !page->valid.TestBlock(last)) last += BlockWords; + GuestMemory::CopyGuestBytes(&page->words[first], reinterpret_cast(base + first * sizeof(std::uint32_t)), (last - first) * sizeof(std::uint32_t)); + page->valid.SetRun(first, last - first); + first = last; } - auto last = first + BlockWords; - while (last < end && !page->valid.TestBlock(last)) last += BlockWords; - GuestMemory::CopyGuestBytes(&page->words[first], reinterpret_cast(base + first * sizeof(std::uint32_t)), (last - first) * sizeof(std::uint32_t)); - page->valid.SetRun(first, last - first); - first = last; } - } - if (page != nullptr && (page->lazy || page->valid.All())) { - std::memcpy(values, page->words.data() + index, bytes); - page->read.SetRun(index, words); - page->recent.SetRun(index, words); - CaptureTotals().reads += words; - } else { - for (std::size_t word = 0; word < words; ++word) { - if (!read(context, address + word * sizeof(std::uint32_t), values + word)) return false; + if (page != nullptr && (page->lazy || (!page->wordwise && page->valid.All()))) { + std::memcpy(values, page->words.data() + index, bytes); + page->read.SetRun(index, words); + page->recent.SetRun(index, words); + CaptureTotals().reads += words; + } else { + const auto offsetWords = static_cast(values - startValues); + for (std::size_t word = 0; word < words; ++word) { + if (strideWords > 1u) { + const auto inRecord = (offsetWords + word) % strideWords; + const auto remainingWords = std::min(strideWords - inRecord, startCount - (offsetWords + word)); + self.descriptorSpanBytes = static_cast(remainingWords * sizeof(std::uint32_t)); + } + const bool ok = read(context, address + word * sizeof(std::uint32_t), values + word); + self.descriptorSpanBytes = savedSpanBytes; + if (!ok) { + return false; + } + if (self.rawInvalidations != invalidationsBefore) break; + } + if (self.rawInvalidations != invalidationsBefore) break; } + address += bytes; + values += words; + count -= words; + } + if (self.rawInvalidations == invalidationsBefore) { + return true; } - address += bytes; - values += words; - count -= words; } - return true; + return false; +} + +std::uint64_t ShaderMemory::descriptorScope(void* context, std::size_t bytes) { + auto& self = *static_cast(context); + if (bytes == ShaderRecompiler::SrtDescriptorForceSync) { + ++self.forceSync; + self.descriptorSpanBytes = 0; + return self.rawInvalidations; + } + if (bytes == ShaderRecompiler::SrtDescriptorEndForceSync) { + if (self.forceSync != 0u) --self.forceSync; + self.descriptorSpanBytes = 0; + return self.rawInvalidations; + } + self.descriptorSpanBytes = bytes; + return self.rawInvalidations; } bool ShaderMemory::accessible(void* context, std::uint64_t address, std::uint64_t bytes) { @@ -644,6 +745,7 @@ std::shared_ptr ShaderMemory::capture(c runtime.stampWrites = &stampWrites; runtime.unchangedSince = &unchangedSince; runtime.supplyWords = &supplyWords; + runtime.descriptorScope = &descriptorScope; auto capture = invocation != nullptr ? invocation->Capture(runtime) : handle != nullptr ? ShaderRecompiler::CaptureResources(request, runtime, *handle) : ShaderRecompiler::CaptureResources(request, runtime); if (profile) { totals.captureNanoseconds += NanosecondsSince(started); diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp index 79269617fe..c44b2faf44 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -61,6 +62,9 @@ inline void StoreStamp(const SrtGuestWords& words, const SrtWriteStamp& stamp) { using SrtMemoryStamper = SrtWriteStamp (*)(void* userData, std::uint64_t address, std::uint64_t bytes); using SrtMemoryUnchanged = bool (*)(void* userData, std::uint64_t address, std::uint64_t bytes, const SrtWriteStamp& stamp); using SrtMemorySupplier = bool (*)(void* userData, const std::shared_ptr& words); +inline constexpr std::size_t SrtDescriptorForceSync = std::numeric_limits::max(); +inline constexpr std::size_t SrtDescriptorEndForceSync = std::numeric_limits::max() - 1u; +using SrtDescriptorScope = std::uint64_t (*)(void* userData, std::size_t bytes); // The guest addresses a walk dereferenced (Detail::Evaluator::EvaluateRawRead): the leaf read of // each pure flat slot (IrResourcePlan::pureFlatSlots) as (flat offset, address), set by the @@ -87,6 +91,20 @@ struct SrtRuntime { SrtMemoryUnchanged unchangedSince = nullptr; SrtMemorySupplier supplyWords = nullptr; SrtUserDataUse* userDataUse = nullptr; + SrtDescriptorScope descriptorScope = nullptr; +}; + +struct SrtForceSyncScope { + const SrtRuntime& runtime; + bool active = false; + SrtForceSyncScope(const SrtRuntime& runtime, bool enable) : runtime(runtime), active(enable && runtime.descriptorScope != nullptr) { + if (active) static_cast(runtime.descriptorScope(runtime.userContext, SrtDescriptorForceSync)); + } + ~SrtForceSyncScope() { + if (active) static_cast(runtime.descriptorScope(runtime.userContext, SrtDescriptorEndForceSync)); + } + SrtForceSyncScope(const SrtForceSyncScope&) = delete; + SrtForceSyncScope& operator=(const SrtForceSyncScope&) = delete; }; enum class RuntimeValueType { diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index 776a3c6dd9..c54e252bdf 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -185,6 +185,7 @@ public: bool EvaluateWide(TNode value, std::uint64_t& result); void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } void ReportNullRootReads(std::uint32_t* sink) { _nullRoots = sink; } + void SetDescriptorBytes(std::size_t bytes) { _descriptorBytes = bytes; } private: static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask, std::unordered_set& firstLanes); @@ -207,6 +208,8 @@ private: const void* _firstLane = nullptr; InaccessibleRead* _inaccessible = nullptr; std::uint32_t* _nullRoots = nullptr; + std::uint32_t _addressDepth = 0; + std::size_t _descriptorBytes = 0; EvaluatedValues _cache; std::vector _visiting; DenseValues _dense; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index e216d7b3a1..74739dfae6 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -137,7 +137,13 @@ bool evaluateRuntimeSources(const IrResourcePlan& program, std::spandwordCount; index++) { inaccessible = {}; - if (!evaluator.Evaluate(TNode::SourceDword(program, sourceIndex, index), value.dwords[index])) { + const auto remainingBytes = source->dwordCount > 1u ? static_cast(source->dwordCount - index) * sizeof(std::uint32_t) : 0u; + evaluator.SetDescriptorBytes(remainingBytes); + cleanEvaluator.SetDescriptorBytes(remainingBytes); + const bool evaluatedDword = evaluator.Evaluate(TNode::SourceDword(program, sourceIndex, index), value.dwords[index]); + evaluator.SetDescriptorBytes(0u); + cleanEvaluator.SetDescriptorBytes(0u); + if (!evaluatedDword) { if (inaccessible.read != nullptr) { if (debug) std::fprintf(stderr, "[srt] descriptor source %u reads inaccessible 0x%llx at pc 0x%x: zero words\n", sourceIndex, static_cast(inaccessible.address), inaccessible.read->Flags().pc); value.dwords.fill(0u); @@ -235,9 +241,38 @@ std::string describeDifference(const std::vector& compactResult } bool interpretRuntimeSources(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, bool evaluateFlat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads) { - if (program.compact.values.empty()) return evaluateRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); + auto* trace = runtime.readTrace; + const auto leavesBefore = trace != nullptr ? trace->leaves.size() : 0; + const auto readsBefore = trace != nullptr ? trace->otherReads.size() : 0; + auto* use = runtime.userDataUse; + const auto baseBefore = use != nullptr ? use->asBase : 0; + const auto valueBefore = use != nullptr ? use->asValue : 0; + const auto useReadsBefore = use != nullptr ? use->reads.size() : 0; + const auto runWithRetry = [&](auto evaluateFn) { + for (std::uint32_t attempt = 0; attempt < 3u; ++attempt) { + if (attempt != 0u) { + if (trace != nullptr) { + trace->leaves.resize(leavesBefore); + trace->otherReads.resize(readsBefore); + trace->leaf = nullptr; + } + if (use != nullptr) { + use->asBase = baseBefore; + use->asValue = valueBefore; + use->reads.resize(useReadsBefore); + } + } + const SrtForceSyncScope forceSync(runtime, attempt == 2u); + const auto invalidationsBefore = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; + const bool ok = evaluateFn(); + if (!ok) return false; + if (runtime.descriptorScope == nullptr || runtime.descriptorScope(runtime.userContext, 0u) == invalidationsBefore) return true; + } + return false; + }; + if (program.compact.values.empty()) return runWithRetry([&] { return evaluateRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); }); prefetchCompactPlan(program.compact); - const bool evaluated = evaluateRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); + const bool evaluated = runWithRetry([&] { return evaluateRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); }); static const bool verify = std::getenv("APS5_VERIFY_SRT_COMPACT") != nullptr; if (verify) { const auto compactReason = RuntimeSourceFailureReason(); diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index 75d62f0546..65c963a2bc 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -206,7 +206,10 @@ bool BasicEvaluator::EvaluateRawRead(const TNode& inst, std::uint64_t& re _runtime.userDataUse->asBase |= std::uint64_t {1} << origin; return true; }; - if (!baseWord() || !Arg(handle, 1, high) || !Arg(inst, 1, offset)) { + ++_addressDepth; + const bool argsEvaluated = baseWord() && Arg(handle, 1, high) && Arg(inst, 1, offset); + --_addressDepth; + if (!argsEvaluated) { return false; } const auto base = ((high << 32u) | static_cast(low)) & AddressMask; @@ -215,7 +218,10 @@ bool BasicEvaluator::EvaluateRawRead(const TNode& inst, std::uint64_t& re if (inst.Opcode() == IrOpcode::ReadConstBuffer) { std::uint64_t records = 0; std::uint64_t word3 = 0; - if (handle.ArgumentCount() != 4u || !Arg(handle, 2, records) || !Arg(handle, 3, word3)) { + ++_addressDepth; + const bool handleEvaluated = handle.ArgumentCount() == 4u && Arg(handle, 2, records) && Arg(handle, 3, word3); + --_addressDepth; + if (!handleEvaluated) { return false; } if (immediate < 0) { @@ -252,7 +258,11 @@ bool BasicEvaluator::EvaluateRawRead(const TNode& inst, std::uint64_t& re } std::uint32_t word = 0; if (_runtime.readMemory != nullptr) { - if (!_runtime.readMemory(_runtime.userContext, address, &word)) { + const bool scoped = _addressDepth == 0u && _descriptorBytes != 0u && _runtime.descriptorScope != nullptr; + if (scoped) static_cast(_runtime.descriptorScope(_runtime.userContext, _descriptorBytes)); + const bool ok = _runtime.readMemory(_runtime.userContext, address, &word); + if (scoped) static_cast(_runtime.descriptorScope(_runtime.userContext, 0u)); + if (!ok) { return false; } } else { @@ -521,6 +531,8 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul BasicEvaluator active(_program, _runtime, _cleanFlatSlots, _cleanEvaluator, &mask); active._outer = this; active._firstLane = inst.Value(); + active._addressDepth = _addressDepth; + active._descriptorBytes = _descriptorBytes; active.ReportInaccessibleReads(_inaccessible); active.ReportNullRootReads(_nullRoots); return active.EvaluateWide(inst.Argument(0), result); @@ -542,7 +554,14 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul return false; } if (slot < _cleanFlatSlots.size() && _cleanFlatSlots[slot] != 0u && _cleanEvaluator != nullptr) { - return _cleanEvaluator->EvaluateWide(TNode::SrtRead(_program, slot), result); + const auto savedDepth = _cleanEvaluator->_addressDepth; + const auto savedBytes = _cleanEvaluator->_descriptorBytes; + _cleanEvaluator->_addressDepth = _addressDepth; + _cleanEvaluator->_descriptorBytes = _descriptorBytes; + const bool ok = _cleanEvaluator->EvaluateWide(TNode::SrtRead(_program, slot), result); + _cleanEvaluator->_addressDepth = savedDepth; + _cleanEvaluator->_descriptorBytes = savedBytes; + return ok; } return EvaluateWide(TNode::SrtRead(_program, slot), result); } @@ -796,12 +815,16 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul } case IrOpcode::SelectU32: case IrOpcode::SelectU1: - case IrOpcode::SelectF32: - if (ternary()) { + case IrOpcode::SelectF32: { + ++_addressDepth; + const bool condOk = Arg(inst, 0, a); + --_addressDepth; + if (condOk && Arg(inst, 1, b) && Arg(inst, 2, c)) { result = a != 0u ? b : c; return true; } return false; + } case IrOpcode::IEqual32: if (binary()) { result = static_cast(a) == static_cast(b) ? 1u : 0u; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp index 208b02d774..ddda8b1fc3 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp @@ -324,6 +324,8 @@ struct SrtTape::Frame { std::uint64_t* values; std::uint8_t* states; bool nullRoot = false; + std::uint32_t addressDepth = 0; + std::size_t descriptorBytes = 0; }; bool SrtTape::evaluate(Frame& frame, std::uint32_t index, std::uint64_t& result) const { @@ -385,6 +387,7 @@ bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) con } std::array operand{}; std::int32_t origin = -1; + const bool addressCone = node.op == Op::Read || node.op == Op::ReadConstBuffer; for (std::uint8_t index = 0; index < node.argumentCount; ++index) { if (index == 0 && node.baseWord >= 0 && frame.runtime.userDataUse != nullptr && static_cast(node.baseWord) < frame.runtime.userData.size()) { origin = node.baseWord; @@ -392,7 +395,11 @@ bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) con frame.runtime.userDataUse->asBase |= std::uint64_t {1} << origin; continue; } - if (!evaluate(frame, node.arguments[index], operand[index])) return false; + const bool nestedAddress = addressCone || (node.op == Op::Select && index == 0u); + if (nestedAddress) ++frame.addressDepth; + const bool ok = evaluate(frame, node.arguments[index], operand[index]); + if (nestedAddress) --frame.addressDepth; + if (!ok) return false; } const auto a = operand[0]; const auto b = operand[1]; @@ -440,7 +447,11 @@ bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) con const auto reader = node.clean ? frame.runtime.readSpecializationMemory : frame.runtime.readMemory; std::uint32_t word = 0; if (reader != nullptr) { - if (!reader(frame.runtime.userContext, address, &word)) return false; + const bool scoped = frame.addressDepth == 0u && frame.descriptorBytes != 0u && frame.runtime.descriptorScope != nullptr; + if (scoped) static_cast(frame.runtime.descriptorScope(frame.runtime.userContext, frame.descriptorBytes)); + const bool ok = reader(frame.runtime.userContext, address, &word); + if (scoped) static_cast(frame.runtime.descriptorScope(frame.runtime.userContext, 0u)); + if (!ok) return false; } else { std::memcpy(&word, reinterpret_cast(address), sizeof(word)); } @@ -576,6 +587,7 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve Frame frame {runtime, values, states}; struct Scratch { + std::uint32_t attempt = 0; std::vector active; std::vector activeFlat; std::vector visited; @@ -585,6 +597,13 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve }; struct ScratchStorage {}; auto& scratch = HostThreadLocal(); + struct AttemptScope { + std::uint32_t& depth; + ~AttemptScope() { --depth; } + } attemptScope{scratch.attempt}; + const auto attempt = scratch.attempt++; + const SrtForceSyncScope forceSync(runtime, attempt == 2u); + const auto invalidationsBefore = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; auto& active = scratch.active; const auto imageBase = plan.descriptorSources.size(); active.assign(imageBase + plan.info.images.size(), 1u); @@ -641,7 +660,10 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve if (active[sourceIndex]) { for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) { std::uint64_t word = 0; - if (!evaluate(frame, sourceDwords[sourceIndex][dword], word)) return refuse(); + frame.descriptorBytes = source.dwordCount > 1u ? static_cast(source.dwordCount - dword) * sizeof(std::uint32_t) : 0u; + const bool ok = evaluate(frame, sourceDwords[sourceIndex][dword], word); + frame.descriptorBytes = 0u; + if (!ok) return refuse(); value.dwords[dword] = static_cast(word); } } @@ -664,6 +686,10 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve if (!ok) return refuse(); flattened[read.flatOffset] = static_cast(word); } + if (runtime.descriptorScope != nullptr && runtime.descriptorScope(runtime.userContext, 0u) != invalidationsBefore) { + refuse(); + return attempt + 1u < 3u && Run(plan, runtime, results, flat, activeSources); + } results.swap(evaluated); activeSources.swap(active); flat.assign(flattened.begin(), flattened.end()); From 6a3542dd9f7ff11c98cc62e874e18adb5e491401 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:39:30 +0300 Subject: [PATCH 194/702] fix(shader): rebuild materialized resource tables on descriptor invalidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom / Log: - 26102026_05_33_48.log:L3022-L3034: [gpu] driver failure: compute shader 0x248a05b00: runtime image descriptor is invalid Root Cause: - core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp:ReadRuns/ReadColumn/Select/TableSnapshotter::Run/Materialize:L882-L1220 — table materialization did not propagate descriptorScope across table entries and did not rebuild earlier runs or records when a later run or indirect record waited on the GPU and invalidated cached Raw words. Change: - Set descriptorScope to the record/descriptor size during ReadRuns, ReadColumn, Select, and TableSnapshotter::Run, rebuild the full table or snapshot across up to 3 attempts when rawInvalidations changes mid-pass, force PendingWrite::Sync on the final attempt via SrtDescriptorForceSync, and refuse if the epoch still fails to stabilize. Metric / Verification: - Materialize and SnapshotResourceTables rebuild earlier runs/records when a late wait invalidates Raw words and refuse unstable snapshots. --- .../Optimization/src/ResourceMaterializer.cpp | 180 ++++++++++++++---- 1 file changed, 143 insertions(+), 37 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 10202666a4..4c3dbe4d2b 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -681,11 +681,18 @@ public: void Run() { tables = ImageTableSnapshot{}; std::uint32_t count = 0; + bool hasIndirect = false; for (const auto& image : plan.info.images) { - if (image.table != NoTable) count = std::max(count, image.table + 1u); + if (image.table != NoTable) { + count = std::max(count, image.table + 1u); + if (image.source < plan.descriptorSources.size() && plan.descriptorSources[image.source].tableColumn.has_value() && plan.descriptorSources[image.source].tableColumn->indirect) hasIndirect = true; + } } for (const auto& sampler : plan.info.samplers) { - if (sampler.table != NoTable) count = std::max(count, sampler.table + 1u); + if (sampler.table != NoTable) { + count = std::max(count, sampler.table + 1u); + if (sampler.source < plan.descriptorSources.size() && plan.descriptorSources[sampler.source].tableColumn.has_value() && plan.descriptorSources[sampler.source].tableColumn->indirect) hasIndirect = true; + } } if (count == 0u) { return; @@ -694,14 +701,31 @@ public: throw std::runtime_error("image table snapshot requires runtime memory access"); } const auto started = std::chrono::steady_clock::now(); - tables.shader = plan.shaderHash; - tables.tables.resize(count); - for (const auto& image : plan.info.images) { - if (image.table != NoTable) Snapshot(image.source, image.table); - } - for (const auto& sampler : plan.info.samplers) { - if (sampler.table != NoTable) Snapshot(sampler.source, sampler.table); + const bool checkEpoch = (count > 1u || hasIndirect) && runtime.descriptorScope != nullptr; + bool completed = false; + for (std::uint32_t attempt = 0; attempt < 3u; ++attempt) { + if (attempt != 0u) { + columns.clear(); + pages.clear(); + wholePages.clear(); + tables = {}; + } + tables.shader = plan.shaderHash; + tables.tables.resize(count); + const SrtForceSyncScope forceSync(runtime, checkEpoch && attempt == 2u); + const auto invalidationsBefore = checkEpoch ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; + for (const auto& image : plan.info.images) { + if (image.table != NoTable) Snapshot(image.source, image.table); + } + for (const auto& sampler : plan.info.samplers) { + if (sampler.table != NoTable) Snapshot(sampler.source, sampler.table); + } + if (!checkEpoch || runtime.descriptorScope(runtime.userContext, 0u) == invalidationsBefore) { + completed = true; + break; + } } + if (!completed) throw std::runtime_error("image table: multi-table snapshot did not stabilize"); auto& counters = tableCounters(); counters.snapshots.fetch_add(1, std::memory_order_relaxed); counters.tables.fetch_add(count, std::memory_order_relaxed); @@ -720,12 +744,17 @@ public: private: static constexpr std::uint64_t PageBytes = 4096u; + mutable std::uint32_t indirectDwords = 0; + mutable std::uint32_t indirectDword = 0; + mutable std::uint32_t accessibleChecks = 0; bool Accessible(std::uint64_t address, std::uint64_t bytes) const { return runtime.accessible == nullptr || runtime.accessible(runtime.userContext, address, bytes); } bool PageAccessible(std::uint64_t address, std::uint64_t begin, std::uint64_t end) { + indirectDword = 0u; + accessibleChecks = 0u; const auto page = address & ~(PageBytes - 1u); if (const auto found = pages.find(page); found != pages.end()) return found->second; const auto first = std::max(page, begin); @@ -739,12 +768,29 @@ private: const auto page = address & ~(PageBytes - 1u); const auto [found, inserted] = wholePages.try_emplace(page, false); if (inserted) found->second = Accessible(page, PageBytes); - return found->second || Accessible(address, sizeof(std::uint32_t)); + const bool accessible = found->second || Accessible(address, sizeof(std::uint32_t)); + if (accessible) { + ++accessibleChecks; + } else { + indirectDword = 0u; + accessibleChecks = 0u; + } + return accessible; } std::uint32_t Read(std::uint64_t address) const { + const bool scoped = runtime.descriptorScope != nullptr && indirectDwords > 1u && accessibleChecks == 1u; + if (scoped) { + static_cast(runtime.descriptorScope(runtime.userContext, static_cast(indirectDwords - indirectDword) * sizeof(std::uint32_t))); + indirectDword = (indirectDword + 1u) % indirectDwords; + } else { + indirectDword = 0u; + } + accessibleChecks = 0u; std::uint32_t word = 0; - if (!runtime.readMemory(runtime.userContext, address, &word)) { + const bool ok = runtime.readMemory(runtime.userContext, address, &word); + if (scoped) static_cast(runtime.descriptorScope(runtime.userContext, 0u)); + if (!ok) { throw std::runtime_error("image table: failed to read guest memory at 0x" + hexText(address)); } return word; @@ -862,25 +908,41 @@ private: return; } DescriptorValue value; - value.dwordCount = 8u; std::uint64_t low = std::numeric_limits::max(); std::uint64_t high = 0; - for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { - const auto position = ImageTableAbi::ScalarBufferDword(offset, column.offset + dword * 4u, size); - if (!position.has_value()) { + bool completed = false; + for (std::uint32_t attempt = 0; attempt < 3u; ++attempt) { + value = {}; + value.dwordCount = 8u; + low = std::numeric_limits::max(); + high = 0; + const SrtForceSyncScope forceSync(runtime, attempt == 2u); + const auto invalidationsBefore = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; + for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { + const auto position = ImageTableAbi::ScalarBufferDword(offset, column.offset + dword * 4u, size); + if (!position.has_value()) { + value.availableDwordMask |= static_cast(1u << dword); + continue; + } + const auto address = base + *position; + if (!PageAccessible(address, base, base + size)) { + records.states.front() = RecordState::Unmapped; + return; + } + if (runtime.descriptorScope != nullptr) static_cast(runtime.descriptorScope(runtime.userContext, static_cast(column.dwordCount - dword) * sizeof(std::uint32_t))); + value.dwords[dword] = Read(address); + if (runtime.descriptorScope != nullptr) static_cast(runtime.descriptorScope(runtime.userContext, 0u)); value.availableDwordMask |= static_cast(1u << dword); - continue; + low = std::min(low, address); + high = std::max(high, address + sizeof(std::uint32_t)); + if (runtime.descriptorScope != nullptr && runtime.descriptorScope(runtime.userContext, 0u) != invalidationsBefore) break; } - const auto address = base + *position; - if (!PageAccessible(address, base, base + size)) { - records.states.front() = RecordState::Unmapped; - return; + if (runtime.descriptorScope == nullptr || runtime.descriptorScope(runtime.userContext, 0u) == invalidationsBefore) { + completed = true; + break; } - value.dwords[dword] = Read(address); - value.availableDwordMask |= static_cast(1u << dword); - low = std::min(low, address); - high = std::max(high, address + sizeof(std::uint32_t)); } + if (!completed) throw std::runtime_error("image table: selected descriptor snapshot did not stabilize"); records.words.front() = WordsIndex(records, value); if (low < high) tables.ranges.emplace_back(low, high - low); } @@ -928,7 +990,15 @@ private: return found->second; } auto& records = columns[source]; - ReadColumn(*plan.descriptorSources.at(source).tableColumn, source, records); + const auto& column = *plan.descriptorSources.at(source).tableColumn; + const auto previousIndirectDwords = indirectDwords; + indirectDwords = column.indirect ? column.dwordCount : 0u; + indirectDword = 0u; + accessibleChecks = 0u; + ReadColumn(column, source, records); + indirectDwords = previousIndirectDwords; + indirectDword = 0u; + accessibleChecks = 0u; if (!records.sealed) Seal(records); return records; } @@ -1101,7 +1171,7 @@ private: read = std::move(comparedWords); } else { read.resize(total); - ReadRuns(runs, read); + ReadRuns(runs, read, column.dwordCount); } auto next = split.begin(); for (std::uint32_t record = 0; record < records.keys; record++) { @@ -1138,16 +1208,39 @@ private: if (memoized) Remember(key, records, std::move(runs), std::move(read), column.address && low < high ? low : 0u, column.address && low < high ? high : 0u); } - void ReadRuns(const std::vector>& runs, std::vector& read) const { - std::size_t at = 0; - for (const auto& [address, count] : runs) { - if (runtime.readWords != nullptr) { - if (!runtime.readWords(runtime.userContext, address, read.data() + at, count)) throw std::runtime_error("image table: failed to read guest memory at 0x" + hexText(address)); - } else { - for (std::uint32_t word = 0; word < count; word++) read[at + word] = Read(address + word * 4u); + void ReadRuns(const std::vector>& runs, std::vector& read, std::uint32_t descriptorDwords) const { + const auto scopeBytes = descriptorDwords > 1u ? static_cast(descriptorDwords) * sizeof(std::uint32_t) : 0u; + bool completed = false; + for (std::uint32_t attempt = 0; attempt < 3u; ++attempt) { + const SrtForceSyncScope forceSync(runtime, attempt == 2u); + const auto invalidationsBefore = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; + std::size_t at = 0; + for (const auto& [address, count] : runs) { + if (runtime.readWords != nullptr) { + if (runtime.descriptorScope != nullptr) static_cast(runtime.descriptorScope(runtime.userContext, scopeBytes)); + const bool ok = runtime.readWords(runtime.userContext, address, read.data() + at, count); + if (runtime.descriptorScope != nullptr) static_cast(runtime.descriptorScope(runtime.userContext, 0u)); + if (!ok) throw std::runtime_error("image table: failed to read guest memory at 0x" + hexText(address)); + } else { + for (std::uint32_t word = 0; word < count; word++) { + if (runtime.descriptorScope != nullptr && descriptorDwords > 1u) { + const auto remaining = std::min(descriptorDwords - (word % descriptorDwords), count - word); + static_cast(runtime.descriptorScope(runtime.userContext, static_cast(remaining) * sizeof(std::uint32_t))); + } + read[at + word] = Read(address + word * 4u); + if (runtime.descriptorScope != nullptr) static_cast(runtime.descriptorScope(runtime.userContext, 0u)); + if (runtime.descriptorScope != nullptr && runtime.descriptorScope(runtime.userContext, 0u) != invalidationsBefore) break; + } + } + if (runtime.descriptorScope != nullptr && runtime.descriptorScope(runtime.userContext, 0u) != invalidationsBefore) break; + at += count; + } + if (runtime.descriptorScope == nullptr || runtime.descriptorScope(runtime.userContext, 0u) == invalidationsBefore) { + completed = true; + break; } - at += count; } + if (!completed) throw std::runtime_error("image table: descriptor snapshot did not stabilize"); tableReuseTotals().wordsRead.fetch_add(read.size(), std::memory_order_relaxed); } @@ -1174,7 +1267,7 @@ private: } } std::vector read(memo.read->words.size()); - ReadRuns(memo.read->runs, read); + ReadRuns(memo.read->runs, read, column.dwordCount); if (read != memo.read->words) { comparedRuns = memo.read->runs; comparedWords = std::move(read); @@ -2490,8 +2583,21 @@ void ResourceMaterializer::Materialize(const IrResourcePlan& program, const SrtR auto& activeSources = HostThreadLocal, ActiveSourcesStorage>(); activeSources.clear(); try { - materializeSnapshot(plan, runtime, walker, nextSnapshot, activeSources); - TableSnapshotter(plan, runtime, walker, activeSources, nextSnapshot.tables).Run(); + bool completed = false; + for (std::uint32_t attempt = 0; attempt < 3u; ++attempt) { + const SrtForceSyncScope forceSync(runtime, attempt == 2u); + const auto beforeSnapshot = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; + activeSources.clear(); + materializeSnapshot(plan, runtime, walker, nextSnapshot, activeSources); + const auto afterSnapshot = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; + TableSnapshotter(plan, runtime, walker, activeSources, nextSnapshot.tables).Run(); + const auto afterTables = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; + if (runtime.descriptorScope == nullptr || (afterTables == afterSnapshot && (plan.uniformFill.fill.kind == UniformFillKind::None || afterSnapshot == beforeSnapshot))) { + completed = true; + break; + } + } + if (!completed) throw std::runtime_error("runtime resource snapshot did not stabilize"); } catch (...) { reportTables(); throw; From f320e359f9e62658452529c87012a0e1c76e0103 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:39:35 +0300 Subject: [PATCH 195/702] test(agc): add torn descriptor, multi-run rebuild, and unstable epoch tests Symptom / Log: - Missing regression coverage in tests/ShaderMemory.cpp for 32-byte T# pendingWrite span unification, full wordwise page reads via readWords(), multi-run and indirect table rebuilds on late GPU wait, second-retry epoch stabilization, and refusal on persistent invalidations. Root Cause: - core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp did not cover mid-descriptor or multi-run GPU wait invalidations. Change: - Add unit tests in tests/ShaderMemory.cpp covering torn 32-byte T# prevention, full 4096-byte wordwise page readWords(), multi-run ReadRuns and indirect ReadColumn rebuilds on late wait, second-retry stabilization, and std::runtime_error refusal after 3 unstable attempts. Metric / Verification: - ctest -R agc_shader_type_zero_image_materialization|agc_shader_memory|agc_shader_table_snapshot_reuse passes with VK_LAYER_KHRONOS_validation and 0 VUID errors. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 469 ++++++++++++++++++ 1 file changed, 469 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2a31594300..b44ee12def 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1233,6 +1233,475 @@ void verifyTypeZeroImageMaterialization() { atomic ? "TYPE=0 or 1 atomic image descriptor was accepted without hardware evidence" : "TYPE=0 or 1 writable image descriptor was accepted without hardware evidence"); } } + + std::vector code{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf4080a00u, 0xfa000080u, 0xbf8cc07fu, 0xf42c0814u, 0xfa000000u, 0xbf8cc07fu, 0xbf068010u, 0xbf850008u}; + for (std::uint32_t dword = 0; dword < 8u; dword++) { + code.push_back(0xbe800300u | ((8u + dword) << 16u) | (32u + dword)); + } + code.insert(code.end(), {0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}); + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture textures[2]; + const auto makeImage = [&](const Texture& texture, std::uint32_t extent) { + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (extent << 30u), extent << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + }; + const auto oldT = makeImage(textures[0], 3u); + const auto newT = makeImage(textures[1], 7u); + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + alignas(4096) static std::array srt{}; + static const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x23001u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + + static std::uint64_t syncBegin = 0; + static std::uint64_t syncEnd = 0; + static std::uint64_t scalarSyncWord = 0; + static std::uint64_t waits = 0; + static std::uint64_t rawFirstWords = 0; + static std::uint64_t scalarSyncHits = 0; + static bool spanAwareSync = true; + static bool flushed = false; + static std::array replacementT{}; + + struct FlushReset { + ~FlushReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); } + } flushReset; + + AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) { + if (!flushed && address < syncEnd && syncBegin < address + bytes) { + flushed = true; + ++waits; + std::memcpy(srt.data(), replacementT.data(), sizeof(replacementT)); + } + }); + + const auto pending = +[](std::uint64_t address, std::size_t bytes, std::span) -> AgcDriver::ShaderMemory::PendingWrite { + using Pending = AgcDriver::ShaderMemory::PendingWrite; + if (bytes >= 4096u) { + return address <= srtAddress && srtAddress < address + bytes ? Pending::Sync : Pending::None; + } + if (scalarSyncWord != 0u && address < scalarSyncWord + sizeof(std::uint32_t) && scalarSyncWord < address + bytes) { + ++scalarSyncHits; + return Pending::Sync; + } + if (flushed) return Pending::None; + if (spanAwareSync) { + if (address < syncEnd && syncBegin < address + bytes) return Pending::Sync; + } else { + if (address >= syncBegin && address < syncEnd) return Pending::Sync; + } + if (address >= srtAddress && address + bytes <= srtAddress + 32u) { + if (bytes == sizeof(std::uint32_t) && address < syncBegin) ++rawFirstWords; + return Pending::Raw; + } + return Pending::None; + }; + const auto hookWaits = +[]() -> std::uint64_t { return waits; }; + + for (const int mode : {0, 1, 2}) { + const bool allRaw = mode == 0; + const bool primaryBarrier = mode == 1; + srt.fill(0u); + std::copy(oldT.begin(), oldT.end(), srt.begin()); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = 1u; + syncBegin = allRaw ? srtAddress + 32u : srtAddress + 12u; + syncEnd = srtAddress + 32u; + scalarSyncWord = srtAddress + 84u; + waits = 0u; + rawFirstWords = 0u; + scalarSyncHits = 0u; + spanAwareSync = primaryBarrier; + flushed = false; + replacementT = newT; + + AgcDriver::ShaderMemory memory({}, pending, nullptr, hookWaits); + const auto capture = memory.Capture(request); + require(scalarSyncHits == 0u, "descriptor pendingWrite barrier: a 4-byte scalar read widened its pendingWrite query into a neighbouring dword"); + const bool holdsNew = std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) { + return std::equal(newT.begin(), newT.end(), value.dwords.begin()); + }); + const bool holdsOld = std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) { + return std::equal(oldT.begin(), oldT.end(), value.dwords.begin()); + }); + if (allRaw) { + require(!flushed && waits == 0u && rawFirstWords == 8u && holdsOld && !holdsNew, + "descriptor pendingWrite barrier: an all-Raw T# did not return the complete pre-sync T#"); + } else { + require(flushed && waits == 1u, "descriptor pendingWrite barrier: the pending GPU write on the second half of T# was not flushed"); + if (primaryBarrier) { + require(rawFirstWords == 0u, "descriptor pendingWrite primary barrier: leading T# dwords took PendingWrite::Raw despite a pending Sync in the 32-byte descriptor span"); + } else { + require(rawFirstWords > 0u, "descriptor pendingWrite secondary barrier test setup did not exercise initial Raw dword reads before the GPU wait"); + } + require(holdsNew && !holdsOld, + primaryBarrier ? "descriptor pendingWrite primary barrier: capture returned a torn or stale T# instead of the complete post-sync T#" + : "descriptor pendingWrite secondary barrier: cached Raw dwords were not invalidated and re-read after the GPU wait"); + } + } + + static Texture extraTexture; + const auto thirdT = makeImage(extraTexture, 11u); + const auto makeTableV = [](const void* data, std::uint32_t stride, std::uint32_t numRecords) { + const auto base = static_cast(reinterpret_cast(data)); + return std::array{ + static_cast(base), + static_cast((base >> 32u) & 0xffffu) | (stride << 16u), + numRecords, + stride == 0u ? 0x31016facu : 0x01016facu, + }; + }; + const std::vector wholeTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, + 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, + 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u, + }; + auto wideTableCode = wholeTableCode; + std::replace(wideTableCode.begin(), wideTableCode.end(), 0x8f108510u, 0x9310b010u); + const std::vector indirectTableCode{ + 0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, + 0xf4080100u, 0xfa000030u, 0x7e200500u, 0x8f108210u, 0xf4200444u, 0x20000000u, + 0x8711ff11u, 0x000000ffu, 0x8f128411u, 0xf4240b02u, 0x24000008u, 0xf4040b96u, + 0xfa000010u, 0xf40c0917u, 0xfa000020u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, + 0x80070000u, 0xbf810000u, + }; + alignas(4096) static std::array tableSrt{}; + static std::array, 3> tableMaterials{}; + static std::array tableOutput{}; + const auto tableSrtAddr = static_cast(reinterpret_cast(tableSrt.data())); + const std::array tableUserData{static_cast(tableSrtAddr), static_cast(tableSrtAddr >> 32u)}; + const auto inPageSrtAddr = srtAddress + 512u * sizeof(std::uint32_t); + const std::array inPageUserData{static_cast(inPageSrtAddr), static_cast(inPageSrtAddr >> 32u)}; + const auto fillWholeSrt = [&](std::uint32_t* dst, const void* heap, std::uint32_t stride, std::uint32_t numRecords) { + const auto heapV = makeTableV(heap, stride, numRecords); + const auto matV = makeTableV(tableMaterials.data(), 16u, 3u); + const auto outV = makeTableV(tableOutput.data(), 0u, 16u); + std::fill_n(dst, 20u, 0u); + std::copy(heapV.begin(), heapV.end(), dst); + std::copy(matV.begin(), matV.end(), dst + 8u); + std::copy(outV.begin(), outV.end(), dst + 12u); + }; + RecompileRequest tableReq = request; + tableReq.shader = {ShaderStage::Compute, 0x23010u, wholeTableCode, 0, {}}; + tableReq.context.waveSize = 64u; + tableReq.context.userData = tableUserData; + tableReq.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + RecompileRequest wideTableReq = tableReq; + wideTableReq.shader = {ShaderStage::Compute, 0x23018u, wideTableCode, 0, {}}; + wideTableReq.context.userData = inPageUserData; + + static std::uint32_t phase = 0; + static std::array pendingT0{}; + static std::array pendingT1{}; + static std::array pendingT2{}; + static std::uint64_t record0Addr = 0; + static std::uint64_t record1Addr = 0; + static std::uint64_t record2Addr = 0; + + static std::uint32_t forcedHookCalls = 0; + static std::uint32_t srtRootReads = 0; + static std::uint64_t lastPendingAddr = 0; + static bool gateSecondWaitOnSrtRoot = false; + + AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) { + if (phase == 1u && !flushed && address < syncEnd && syncBegin < address + bytes) { + flushed = true; + ++waits; + std::memcpy(reinterpret_cast(record0Addr), pendingT0.data(), sizeof(pendingT0)); + if (record1Addr != 0u) std::memcpy(reinterpret_cast(record1Addr), pendingT1.data(), sizeof(pendingT1)); + } else if (phase == 2u || phase == 3u) { + if (waits == 0u && address < record1Addr + 32u && record1Addr + 12u < address + bytes) { + ++waits; + std::memcpy(reinterpret_cast(record1Addr), pendingT1.data(), sizeof(pendingT1)); + } else if ((waits == 1u || (phase == 3u && waits >= 1u)) && address < record2Addr + 32u && record2Addr + 12u < address + bytes) { + ++waits; + std::memcpy(reinterpret_cast(record0Addr), pendingT0.data(), sizeof(pendingT0)); + std::memcpy(reinterpret_cast(record2Addr), pendingT2.data(), sizeof(pendingT2)); + } else if (phase == 2u && waits == 2u && address < record0Addr + 32u && record0Addr < address + bytes) { + ++forcedHookCalls; + } + } else if (phase == 5u) { + ++waits; + } + }); + + const auto multiPending = +[](std::uint64_t address, std::size_t bytes, std::span known) -> AgcDriver::ShaderMemory::PendingWrite { + using Pending = AgcDriver::ShaderMemory::PendingWrite; + if (bytes >= 4096u) { + return address <= srtAddress && srtAddress < address + bytes ? Pending::Sync : Pending::None; + } + if (address != lastPendingAddr) { + lastPendingAddr = address; + if (address == srtAddress + 512u * sizeof(std::uint32_t) + 48u) { + ++srtRootReads; + } + } + if (phase == 0u || phase == 5u) return Pending::Raw; + if (phase == 4u) { + if (address >= record0Addr && address + bytes <= record0Addr + 32u) { + if (known.size() == bytes) { + std::memcpy(known.data(), reinterpret_cast(pendingT0.data()) + (address - record0Addr), bytes); + } + return Pending::KnownValue; + } + return Pending::Raw; + } + if (phase == 1u) { + if (flushed) return Pending::None; + if (spanAwareSync) { + if (address < syncEnd && syncBegin < address + bytes) return Pending::Sync; + } else { + if (address >= syncBegin && address < syncEnd) return Pending::Sync; + } + return Pending::Raw; + } + if (waits == 0u) { + if (address >= record1Addr + 12u && address < record1Addr + 32u && bytes == sizeof(std::uint32_t)) return Pending::Sync; + return Pending::Raw; + } + if (waits == 1u || (phase == 3u && waits >= 1u)) { + if (address >= record1Addr && address + bytes <= record1Addr + 32u) return Pending::None; + if ((!gateSecondWaitOnSrtRoot || srtRootReads >= 2u) && address >= record2Addr + 12u && address < record2Addr + 32u && bytes == sizeof(std::uint32_t)) return Pending::Sync; + return Pending::Raw; + } + if (phase == 2u && waits == 2u) { + if (gateSecondWaitOnSrtRoot && + ((srtRootReads == 3u && address == srtAddress + 512u * sizeof(std::uint32_t) + 56u) || + (srtRootReads == 4u && address == srtAddress + 512u * sizeof(std::uint32_t) + 60u))) { + return Pending::Sync; + } + if ((gateSecondWaitOnSrtRoot && address >= srtAddress + 512u * sizeof(std::uint32_t) + 48u && address < srtAddress + 512u * sizeof(std::uint32_t) + 64u) || + (address < record0Addr + 32u && record0Addr < address + bytes)) { + return Pending::Raw; + } + } + return Pending::None; + }; + + srt.fill(0u); + std::copy(oldT.begin(), oldT.end(), srt.begin()); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = 1u; + phase = 0u; + waits = 0u; + flushed = false; + record0Addr = srtAddress; + record1Addr = 0u; + pendingT0 = thirdT; + { + AgcDriver::ShaderMemory knownTransitionMemory({}, multiPending, nullptr, hookWaits); + const auto rawCapture = knownTransitionMemory.Capture(request); + require(!flushed && waits == 0u && std::ranges::any_of(rawCapture->snapshot.images, [&](const DescriptorValue& value) { + return std::equal(oldT.begin(), oldT.end(), value.dwords.begin()); + }), "raw-to-known descriptor transition: initial Raw capture did not cache oldT"); + phase = 4u; + const auto knownCapture = knownTransitionMemory.Capture(request); + require(!flushed && waits == 0u && std::ranges::any_of(knownCapture->snapshot.images, [&](const DescriptorValue& value) { + return std::equal(thirdT.begin(), thirdT.end(), value.dwords.begin()); + }), "raw-to-known descriptor transition: cached Raw descriptor was not invalidated and replaced by KnownValue"); + } + + for (std::uint32_t record = 0; record < 128u; ++record) { + std::copy(oldT.begin(), oldT.end(), srt.begin() + record * 8u); + } + fillWholeSrt(tableSrt.data(), srt.data(), 32u, 128u); + phase = 0u; + waits = 0u; + flushed = false; + record0Addr = srtAddress; + record1Addr = 0u; + pendingT0 = thirdT; + { + AgcDriver::ShaderMemory fullPageMemory({}, multiPending, nullptr, hookWaits); + const auto initialCapture = fullPageMemory.Capture(tableReq); + const auto& initialCol = initialCapture->snapshot.tables.tables.at(0); + require(initialCol.keys == 128u && initialCol.words.at(initialCol.codes.at(0)).dwords == oldT, + "full raw page readWords test: initial raw capture did not populate the table with oldT"); + phase = 4u; + const auto knownCapture = fullPageMemory.Capture(tableReq); + const auto& knownCol = knownCapture->snapshot.tables.tables.at(0); + require(!flushed && waits == 0u && knownCol.words.at(knownCol.codes.at(0)).dwords == thirdT, + "full raw page readWords: cached Raw descriptor words did not consume KnownValue replacement"); + phase = 1u; + pendingT0 = newT; + spanAwareSync = true; + syncBegin = srtAddress + 12u; + syncEnd = srtAddress + 32u; + const auto updatedCapture = fullPageMemory.Capture(tableReq); + const auto& updatedCol = updatedCapture->snapshot.tables.tables.at(0); + require(flushed && waits == 1u && updatedCol.words.at(updatedCol.codes.at(0)).dwords == newT, + "full raw page readWords: readWords() bypassed pendingWrite policy on a fully cached wordwise page"); + } + + for (const bool spanSync : {false, true}) { + std::fill(srt.begin(), srt.end(), 0u); + std::copy(oldT.begin(), oldT.end(), srt.begin()); + std::copy(oldT.begin(), oldT.end(), srt.begin() + 12u); + fillWholeSrt(srt.data() + 512u, srt.data(), 48u, 2u); + phase = 1u; + waits = 0u; + flushed = false; + spanAwareSync = spanSync; + record0Addr = srtAddress; + record1Addr = srtAddress + 48u; + syncBegin = record1Addr + 12u; + syncEnd = record1Addr + 32u; + pendingT0 = newT; + pendingT1 = thirdT; + AgcDriver::ShaderMemory multiRunMemory({}, multiPending, nullptr, hookWaits); + const auto multiRunCapture = multiRunMemory.Capture(wideTableReq); + const auto& col = multiRunCapture->snapshot.tables.tables.at(0); + require(flushed && waits == 1u && col.keys == 2u, "multi-run table wait: second run did not flush"); + require(col.words.at(col.codes.at(0)).dwords == newT && col.words.at(col.codes.at(1)).dwords == thirdT, + "multi-run table wait: a GPU wait on a later run did not rebuild descriptors from an earlier run"); + } + + { + alignas(4096) static std::array indRoot{}; + alignas(4096) static std::array indRecords{}; + alignas(4096) static std::array, 2> indLinks{}; + static std::array indKeys{}; + const auto ptrAddr = [](const void* p) { return static_cast(reinterpret_cast(p)); }; + const auto storePtr = [](std::uint32_t* at, std::uint64_t pointer) { + at[0] = static_cast(pointer); + at[1] = static_cast(pointer >> 32u); + }; + std::fill(srt.begin(), srt.end(), 0u); + std::copy(oldT.begin(), oldT.end(), srt.begin() + 8u); + std::copy(oldT.begin(), oldT.end(), srt.begin() + 24u); + storePtr(indRecords.data() + 2u, ptrAddr(indLinks[0].data())); + storePtr(indRecords.data() + 6u, ptrAddr(indLinks[1].data())); + storePtr(indLinks[0].data() + 4u, srtAddress); + storePtr(indLinks[1].data() + 4u, srtAddress + 64u); + const auto keysV = makeTableV(indKeys.data(), 0u, static_cast(sizeof(indKeys))); + const auto outV = makeTableV(tableOutput.data(), 0u, static_cast(sizeof(tableOutput))); + const auto recsV = makeTableV(indRecords.data(), 0u, static_cast(sizeof(indRecords))); + indRoot.fill(0u); + std::copy(keysV.begin(), keysV.end(), indRoot.begin()); + std::copy(outV.begin(), outV.end(), indRoot.begin() + 4u); + std::copy(recsV.begin(), recsV.end(), indRoot.begin() + 12u); + const std::array indUserData{static_cast(ptrAddr(indRoot.data())), static_cast(ptrAddr(indRoot.data()) >> 32u)}; + RecompileRequest indReq = tableReq; + indReq.shader = {ShaderStage::Compute, 0x23020u, indirectTableCode, 0, {}}; + indReq.context.userData = indUserData; + phase = 1u; + waits = 0u; + flushed = false; + spanAwareSync = false; + record0Addr = srtAddress + 32u; + record1Addr = srtAddress + 96u; + syncBegin = record1Addr + 12u; + syncEnd = record1Addr + 32u; + pendingT0 = newT; + pendingT1 = thirdT; + AgcDriver::ShaderMemory indirectMemory({}, multiPending, nullptr, hookWaits); + const auto indCapture = indirectMemory.Capture(indReq); + const auto& col = indCapture->snapshot.tables.tables.at(0); + require(flushed && waits == 1u && col.keys == 2u, "indirect table wait: second record did not flush"); + require(col.words.at(col.codes.at(0)).dwords == newT && col.words.at(col.codes.at(1)).dwords == thirdT, + "indirect table wait: a GPU wait on a later indirect record did not rebuild an earlier record"); + } + + const auto postForcedSrtAddr = srtAddress + 256u * sizeof(std::uint32_t); + const std::array postForcedUserData{static_cast(postForcedSrtAddr), static_cast(postForcedSrtAddr >> 32u)}; + RecompileRequest postForcedReq = request; + postForcedReq.context.userData = postForcedUserData; + + for (const int mode : {0, 1, 2}) { + const bool stridedRuns = mode != 0; + gateSecondWaitOnSrtRoot = mode == 2; + const std::uint32_t stride = stridedRuns ? 48u : 32u; + const std::size_t stepWords = stride / sizeof(std::uint32_t); + std::fill(srt.begin(), srt.end(), 0u); + std::copy(oldT.begin(), oldT.end(), srt.begin()); + std::copy(oldT.begin(), oldT.end(), srt.begin() + stepWords); + std::copy(oldT.begin(), oldT.end(), srt.begin() + 2u * stepWords); + std::copy(oldT.begin(), oldT.end(), srt.begin() + 256u); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 256u + 16u); + srt[256u + 20u] = 1u; + fillWholeSrt(srt.data() + 512u, srt.data(), stride, 3u); + RecompileRequest twoWaitReq = stridedRuns ? wideTableReq : tableReq; + twoWaitReq.context.userData = inPageUserData; + phase = 2u; + waits = 0u; + forcedHookCalls = 0u; + srtRootReads = 0u; + lastPendingAddr = 0u; + flushed = false; + record0Addr = srtAddress; + record1Addr = srtAddress + stride; + record2Addr = srtAddress + 2u * stride; + pendingT0 = newT; + pendingT1 = thirdT; + pendingT2 = newT; + AgcDriver::ShaderMemory twoWaitMemory({}, multiPending, nullptr, hookWaits); + const auto twoWaitCapture = twoWaitMemory.Capture(twoWaitReq); + const auto& col = twoWaitCapture->snapshot.tables.tables.at(0); + require(waits == 2u && forcedHookCalls == 15u && (!gateSecondWaitOnSrtRoot || srtRootReads == 5u) && col.keys == 3u, + "multi-wait readWords: forced third attempt did not execute or nested forceSync scope was not preserved"); + require(col.words.at(col.codes.at(0)).dwords == newT && col.words.at(col.codes.at(1)).dwords == thirdT && col.words.at(col.codes.at(2)).dwords == newT, + "multi-wait readWords: retry pass accepted an unstable epoch after a second invalidation"); + phase = 5u; + waits = 0u; + forcedHookCalls = 0u; + const auto normalAfterForced = twoWaitMemory.Capture(postForcedReq); + const auto cachedAfterForced = twoWaitMemory.Capture(postForcedReq); + require(waits == 0u && forcedHookCalls == 0u && std::ranges::any_of(normalAfterForced->snapshot.images, [&](const DescriptorValue& value) { + return std::equal(oldT.begin(), oldT.end(), value.dwords.begin()); + }) && std::ranges::any_of(cachedAfterForced->snapshot.images, [&](const DescriptorValue& value) { + return std::equal(oldT.begin(), oldT.end(), value.dwords.begin()); + }), "forced pass scope: a normal pass over Raw words after a forced third attempt waited on the hook or bumped rawInvalidations"); + + std::fill(srt.begin(), srt.end(), 0u); + std::copy(oldT.begin(), oldT.end(), srt.begin()); + std::copy(oldT.begin(), oldT.end(), srt.begin() + stepWords); + std::copy(oldT.begin(), oldT.end(), srt.begin() + 2u * stepWords); + std::copy(oldT.begin(), oldT.end(), srt.begin() + 256u); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 256u + 16u); + srt[256u + 20u] = 1u; + fillWholeSrt(srt.data() + 512u, srt.data(), stride, 3u); + phase = 3u; + waits = 0u; + forcedHookCalls = 0u; + srtRootReads = 0u; + lastPendingAddr = 0u; + flushed = false; + AgcDriver::ShaderMemory unstableMemory({}, multiPending, nullptr, hookWaits); + bool refused = false; + try { + static_cast(unstableMemory.Capture(twoWaitReq)); + } catch (const std::runtime_error&) { + refused = true; + } + require(refused && waits >= 3u, "unstable epoch refusal: capture succeeded despite invalidation on every retry attempt"); + phase = 5u; + waits = 0u; + forcedHookCalls = 0u; + const auto normalAfterUnstable = unstableMemory.Capture(postForcedReq); + require(waits == 0u && forcedHookCalls == 0u && std::ranges::any_of(normalAfterUnstable->snapshot.images, [&](const DescriptorValue& value) { + return std::equal(oldT.begin(), oldT.end(), value.dwords.begin()); + }), "forced pass exception scope: a normal pass over Raw words after a throwing forced attempt waited on the hook or bumped rawInvalidations"); + } + gateSecondWaitOnSrtRoot = false; } void verifyOutOfRangeConstantReads() { From 94d991e03a450a50c0fed6b8514bc26358dd628d Mon Sep 17 00:00:00 2001 From: BunnyFl <166477956+BunnyFl@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:46:50 +0100 Subject: [PATCH 196/702] fix(tools): read preprocessor branch combinations lazily in import_audit Co-Authored-By: Claude Opus 5.5 --- tools/import_audit.py | 16 ++++++++++------ tools/tests/test_import_audit.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 57cf09a9b1..ef0d726615 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -1,5 +1,6 @@ import argparse import hashlib +import itertools import json import os import re @@ -536,22 +537,25 @@ def branch_variants(body): root.append(body[position:]) def expand(items): - variants = [""] + parts = [] for item in items: if isinstance(item, str): - variants = [variant + item for variant in variants] + parts.append((item,)) continue choices = [choice for alternative in item["alternatives"] for choice in expand(alternative)] - variants = [variant + choice for variant in variants for choice in choices] - if len(variants) > BRANCH_VARIANT_LIMIT: + if len(choices) > BRANCH_VARIANT_LIMIT: raise AuditError(f"more than {BRANCH_VARIANT_LIMIT} preprocessor branch combinations in one function") - return variants + parts.append(choices) + for count, variant in enumerate(itertools.product(*parts), 1): + if count > BRANCH_VARIANT_LIMIT: + raise AuditError(f"more than {BRANCH_VARIANT_LIMIT} preprocessor branch combinations in one function") + yield "".join(variant) return expand(root) def variant_calls(body): - return [unconditional_calls(variant) for variant in branch_variants(body)] + return (unconditional_calls(variant) for variant in branch_variants(body)) def unconditional_calls(body): diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 518fd2f165..4be39dc364 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1601,6 +1601,24 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(partial, {(library, nid_names.compute_nid(name)) for library in ("libGuard.prx", "libShared.prx") for name, kind in kinds.items() if kind == "partial"}) + def test_many_host_branches_only_fail_when_every_combination_must_be_read(self): + blocks = "".join(f"#ifdef HOST_{index}\n value += {index};\n#endif\n" for index in range(9)) + source = self.work.source + write_source(source, "libHosts", + "void Fail() { NotImplemented_nid_no_patch(__func__); }\n" + "int APS5_VABI sceStub(int value) { NotImplemented_nid_no_patch(__func__); return 0; }\n" + f"int APS5_VABI sceHosts(int value) {{\n{blocks} return value;\n}}\n" + f"int APS5_VABI sceLastHost(int value) {{\n{blocks}#ifdef HOST_LAST\n Fail();\n#endif\n return value;\n}}\n") + stubs, partial = import_audit.source_stub_classes(source) + stub, hosts, last = (("libHosts.prx", nid_names.compute_nid(name)) for name in ("sceStub", "sceHosts", "sceLastHost")) + self.assertEqual(set(stubs), {stub, last}) + self.assertEqual(partial, {last}) + write_source(source, "libHosts", + "void Fail() { NotImplemented_nid_no_patch(__func__); }\n" + f"int APS5_VABI sceEveryHost(int value) {{\n{blocks} Fail();\n return value;\n}}\n") + with self.assertRaisesRegex(import_audit.AuditError, "more than 256 preprocessor branch combinations"): + import_audit.source_stub_classes(source) + def test_partial_import_is_its_own_class(self): direct, guarded = nid_names.compute_nid("sceDirect"), nid_names.compute_nid("sceGuarded") stubs = {("libA.prx", direct): "sceDirect", ("libA.prx", guarded): "sceGuarded", ("libB.prx", guarded): "sceGuarded"} From 60cf9c3bd382139cc710772c14622af7c0c3fb06 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Mon, 26 Oct 2026 22:53:02 +0000 Subject: [PATCH 197/702] test(shader): drop the unscaled first-lane refusal check AI-assisted. Tested by me on my own machine. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index f4f111df0c..355425ff6d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2886,8 +2886,6 @@ void verifyImageTableFirstLaneScale() { require(snapshot.records == 4u && snapshot.keys == 4u && snapshot.fault == 0u, "image table first-lane scale: the snapshot does not hold the four 96-byte records"); const auto codes = TableCodes(TableMap(*compiled.result), 0); require(codes.size() == 4u && codes[0] == codes[3] && codes[2] == Abi::NullCode && codes[1] != codes[0] && (codes[0] & Abi::PoisonFlag) == 0u && (codes[1] & Abi::PoisonFlag) == 0u, "image table first-lane scale: the table map is wrong"); - const auto unscaled = scaled({0x7e200500u}); - expectFailure([&] { static_cast(GetResourcePlan(TableRequest(unscaled, userData))); }, "GetImageResource dword 0 is not a valid runtime value", "image table first-lane scale: a first-lane offset with no dword multiple was accepted"); } void verifyImageTableRegistration() { From 106048ef941c45897659303d5eb799d8f519dca7 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 26 Oct 2026 22:59:16 +0000 Subject: [PATCH 198/702] test(libSceFont): cover the render effect slant and weight setters against their getters Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestFont.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 093c88fe45..147e09012c 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -232,6 +232,31 @@ static bool KerningIs(const FontKerning& kerning, float offsetX) { return kerning.offsetX == offsetX && kerning.offsetY == 0.0f && kerning.positionX == 0.0f && kerning.positionY == 0.0f; } +static void CheckRenderEffects(FontHandle font, FontRenderer renderer) { + const auto near = [](float value, float expected) { return std::fabs(value - expected) < 1e-6f; }; + float slant = 1.0f; + float weightX = 0.0f; + float weightY = 0.0f; + std::uint32_t mode = 7; + Require(sceFontSetupRenderEffectSlant(font, 0.25f) == SCE_FONT_ERROR_NOT_BOUND_RENDERER); + Require(sceFontSetupRenderEffectWeight(font, 1.02f, 0.98f, 0) == SCE_FONT_ERROR_NOT_BOUND_RENDERER); + Require(sceFontGetRenderEffectSlant(font, &slant) == SCE_FONT_ERROR_NOT_BOUND_RENDERER && slant == 0.0f); + Require(sceFontBindRenderer(font, renderer) == SCE_FONT_OK); + Require(sceFontSetupRenderEffectSlant(font, 0.25f) == SCE_FONT_OK); + Require(sceFontGetRenderEffectSlant(font, &slant) == SCE_FONT_OK && slant == 0.25f); + Require(sceFontSetupRenderEffectSlant(font, 2.0f) == SCE_FONT_OK); + Require(sceFontGetRenderEffectSlant(font, &slant) == SCE_FONT_OK && slant == 1.0f); + Require(sceFontSetupRenderEffectWeight(font, 1.02f, 0.98f, 1) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetupRenderEffectWeight(font, 1.02f, 0.98f, 0) == SCE_FONT_OK); + Require(sceFontGetRenderEffectWeight(font, &weightX, &weightY, &mode) == SCE_FONT_OK && near(weightX, 1.02f) && near(weightY, 0.98f) && mode == 0); + Require(sceFontSetupRenderEffectWeight(font, 1.5f, 0.5f, 0) == SCE_FONT_OK); + Require(sceFontGetRenderEffectWeight(font, &weightX, &weightY, nullptr) == SCE_FONT_OK && near(weightX, 1.04f) && near(weightY, 0.96f)); + Require(sceFontSetupRenderEffectSlant(nullptr, 0.0f) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetupRenderEffectWeight(nullptr, 1.0f, 1.0f, 0) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetupRenderEffectSlant(font, 0.0f) == SCE_FONT_OK && sceFontSetupRenderEffectWeight(font, 1.0f, 1.0f, 0) == SCE_FONT_OK); + Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); +} + static void CheckGlyphScale(FontHandle font, const FontMemory* memory, float expectedW, float expectedH) { float width = 0.0f; float height = 0.0f; @@ -650,6 +675,7 @@ int main() { Require(sceFontGetRenderScaledKerning(font, 'A', 'V', &kerning) == SCE_FONT_ERROR_NOT_BOUND_RENDERER); CheckPointAndDpiScale(font, library, &memory, renderer); + CheckRenderEffects(font, renderer); ParsedText parsed{"AV", 0, font}; FontTextSource source{}; From 5c9af08324500f9bc23dc2b314d4726e7493fa1c Mon Sep 17 00:00:00 2001 From: WhiteAssassins <28965087+WhiteAssassins@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:17:36 -0400 Subject: [PATCH 199/702] feat(tools): add optional patch catalog metadata --- .../relinker/tests/test_patch_advisor.py | 11 +- docs/user/USAGE.md | 30 +++++ tools/suggest_patches.py | 105 +++++++++++++++++- tools/tests/test_suggest_patches.py | 81 ++++++++++++++ 4 files changed, 219 insertions(+), 8 deletions(-) diff --git a/core/relinker/relinker/tests/test_patch_advisor.py b/core/relinker/relinker/tests/test_patch_advisor.py index e18a165784..0f642b468c 100644 --- a/core/relinker/relinker/tests/test_patch_advisor.py +++ b/core/relinker/relinker/tests/test_patch_advisor.py @@ -25,10 +25,10 @@ def main(): metadata = root / "param.json" metadata.write_text(json.dumps({"titleId": "TEST00001", "contentVersion": "01.000.000"})) - def advise(digest, patches=("patch.toml",), *options): + def advise(digest, patches=("patch.toml",), *options, entry_metadata=None): (catalog / "index.json").write_text(json.dumps({"format": 1, "entries": [{ "titleId": "TEST00001", "contentVersion": "01.000.000", "patch": name, - "targets": {"eboot.bin": digest} + "targets": {"eboot.bin": digest}, **(entry_metadata or {}) } for name in patches]})) return subprocess.run([sys.executable, str(advisor), "--catalog", str(catalog), "--param-json", str(metadata), "--input", str(source), @@ -38,6 +38,13 @@ def main(): digest = hashlib.sha256(image).hexdigest() result = advise(digest) assert result.returncode == 0 and f"Recommended: {patch}" in result.stdout, (result.stdout, result.stderr) + result = advise(digest, ("patch.toml",), "--list-verified", entry_metadata={ + "id": "synthetic_fix", "name": "Synthetic fix", "category": "compatibility", + "description": "Changes one byte", "revision": 1, + "tested": [{"stage": "verification", "result": "passed", "evidence": "Relinker confirmed one patch"}]}) + assert result.returncode == 0 and "Name: Synthetic fix" in result.stdout, (result.stdout, result.stderr) + assert "Tested: verification passed: Relinker confirmed one patch" in result.stdout, (result.stdout, result.stderr) + assert source.read_bytes() == image second = catalog / "second patch.toml" second.write_text(patch_text(address, expected, "flip_bit")) result = advise(digest, ("patch.toml", second.name)) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index d10adeb020..8c757cc18e 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -124,6 +124,36 @@ The advisor reads `titleId` and `contentVersion` from the specified `param.json` } ``` +Catalog entries may also contain these optional fields. Catalogs without them keep the same behavior and `format: 1`. + +| Field | Value | +|-------|-------| +| `id` | Stable patch identifier matching `[a-z0-9][a-z0-9_-]*`. It may be reused for another content version. Verified entries with the same ID for one title and version are rejected. | +| `name` | User-facing name for the whole patch file. | +| `category` | One of `compatibility`, `fps`, `graphics`, `memory` or `other`. | +| `description` | What the patch file changes. | +| `tested` | Nonempty array of test records, described below. | +| `notes` | Limits or instructions for the user. | +| `contributors` | Nonempty array of distinct contributor names. | +| `source` | HTTPS link to the patch's provenance. The advisor only displays it; it does not fetch it. | +| `revision` | Positive integer revision of the patch definition, separate from the game's `contentVersion`. | + +`name`, `description` and `notes` are nonempty single-line text when present. They describe the catalog entry as a whole; `name` and `description` inside a patch TOML's `[[patch]]` describe individual byte changes. Each `tested` record requires `stage` (`verification`, `conversion`, `launch` or `gameplay`), `result` (`passed`, `failed` or `partial`) and nonempty single-line `evidence`. Optional `environment` is single-line text, and optional `date` is a calendar date in `YYYY-MM-DD` form. For example: + +```json +{ + "tested": [{ + "stage": "verification", + "result": "passed", + "environment": "CachyOS Linux", + "date": "2026-10-26", + "evidence": "Relinker reported 1 ok, 0 not-checked, 0 mismatch" + }] +} +``` + +The advisor validates these fields for matching catalog entries and reports a malformed entry without stopping the remaining entries. It prints metadata only for entries that pass the hash and relinker checks. Test records are contributor-provided claims; the advisor does not independently confirm launch or gameplay evidence. + The advisor validates patch files only for entries matching the requested title and version. It reports and skips a malformed matching entry or a relinker verification error, so another valid entry can still be recommended. Verification times out after 120 seconds per entry by default; use `--timeout-seconds ` to change the limit. For synthetic inputs without system modules, use `--skip-sce-module` if conversion also uses it; this option conflicts with `--sce-module-path`. No matching entry or no verified patch exits `1`; invalid input or no verified patch after a relinker error exits `2`. If multiple entries verify, the advisor recommends none because it cannot choose or combine them, and points to `--list-verified` to inspect the matches. The tool does not fetch or apply patches. A recommendation prints `--patches` and the module and executable options to use when converting. Keep catalog patch files outside the input executable's directory to avoid automatic `relinker-patches.toml` discovery. With `--list-verified`, the advisor prints every entry that passes metadata, hash and relinker checks, in catalog order. Each result includes its catalog number, patch file, target hashes and conversion options. It exits `0` when at least one entry verifies, even if another entry was rejected; with no verified entries, the existing `1` or `2` exit status still applies. Without this option, a single verified entry is recommended and multiple verified entries remain ambiguous. The listing tells users to choose one patch file. It does not combine patch files or apply them during conversion; review the chosen patch before applying it. diff --git a/tools/suggest_patches.py b/tools/suggest_patches.py index f927d31dc6..7262fb5662 100644 --- a/tools/suggest_patches.py +++ b/tools/suggest_patches.py @@ -7,11 +7,20 @@ import shlex import subprocess import sys import tomllib +from collections import Counter +from datetime import date from pathlib import Path, PurePosixPath +from urllib.parse import urlsplit SHA256 = re.compile(r"[0-9a-fA-F]{64}\Z") VERIFY_SUMMARY = re.compile(r"^Verified (\d+) patches: (\d+) ok, (\d+) not-checked, (\d+) mismatch$", re.MULTILINE) +PATCH_ID = re.compile(r"[a-z0-9][a-z0-9_-]*\Z") +TEST_DATE = re.compile(r"\d{4}-\d{2}-\d{2}\Z") +ENTRY_FIELDS = {"titleId", "contentVersion", "patch", "targets", "id", "name", "category", "description", "tested", "notes", "contributors", "source", "revision"} +CATEGORIES = {"compatibility", "fps", "graphics", "memory", "other"} +TEST_STAGES = {"verification", "conversion", "launch", "gameplay"} +TEST_RESULTS = {"passed", "failed", "partial"} def confined(root, name): @@ -63,9 +72,80 @@ def load_catalog(directory, title, version): return entries +def single_line(value, field): + if not isinstance(value, str) or not value.strip() or not value.isprintable(): + raise ValueError(f"{field} must be nonempty single-line text") + return value + + +def validate_metadata(entry): + metadata = {key: entry[key] for key in ENTRY_FIELDS - {"titleId", "contentVersion", "patch", "targets"} if key in entry} + if "id" in metadata and (not isinstance(metadata["id"], str) or not PATCH_ID.fullmatch(metadata["id"])): + raise ValueError("id must match [a-z0-9][a-z0-9_-]*") + for field in ("name", "description", "notes"): + if field in metadata: + single_line(metadata[field], field) + if "category" in metadata and (not isinstance(metadata["category"], str) or metadata["category"] not in CATEGORIES): + raise ValueError("invalid category") + if "source" in metadata: + source = single_line(metadata["source"], "source") + url = urlsplit(source) + if url.scheme != "https" or not url.hostname or any(character.isspace() for character in source): + raise ValueError("source must be an HTTPS URL") + if "revision" in metadata and (type(metadata["revision"]) is not int or metadata["revision"] <= 0): + raise ValueError("revision must be a positive integer") + if "contributors" in metadata: + contributors = metadata["contributors"] + if not isinstance(contributors, list) or not contributors: + raise ValueError("contributors must be a nonempty array") + for contributor in contributors: + single_line(contributor, "contributor") + if len({contributor.casefold() for contributor in contributors}) != len(contributors): + raise ValueError("duplicate contributor") + if "tested" in metadata: + records = metadata["tested"] + if not isinstance(records, list) or not records: + raise ValueError("tested must be a nonempty array") + for record in records: + if not isinstance(record, dict) or not {"stage", "result", "evidence"} <= set(record) or set(record) - {"stage", "result", "evidence", "environment", "date"}: + raise ValueError("invalid tested record fields") + if not isinstance(record["stage"], str) or record["stage"] not in TEST_STAGES or not isinstance(record["result"], str) or record["result"] not in TEST_RESULTS: + raise ValueError("invalid tested stage or result") + single_line(record["evidence"], "tested evidence") + if "environment" in record: + single_line(record["environment"], "tested environment") + if "date" in record: + value = record["date"] + if not isinstance(value, str) or not TEST_DATE.fullmatch(value): + raise ValueError("tested date must be YYYY-MM-DD") + try: + date.fromisoformat(value) + except ValueError as error: + raise ValueError("tested date must be a valid calendar date") from error + return metadata + + +def print_metadata(metadata): + for field, label in (("id", "ID"), ("name", "Name"), ("category", "Category"), + ("description", "Description"), ("revision", "Revision"), + ("notes", "Notes"), ("source", "Source")): + if field in metadata: + print(f" {label}: {metadata[field]}") + for contributor in metadata.get("contributors", []): + print(f" Contributor: {contributor}") + for record in metadata.get("tested", []): + detail = f"{record['stage']} {record['result']}" + if "environment" in record: + detail += f" on {record['environment']}" + if "date" in record: + detail += f" ({record['date']})" + print(f" Tested: {detail}: {record['evidence']}") + + def validate_entry(directory, entry): - if set(entry) != {"titleId", "contentVersion", "patch", "targets"}: + if not {"titleId", "contentVersion", "patch", "targets"} <= set(entry) or set(entry) - ENTRY_FIELDS: raise ValueError("invalid fields") + metadata = validate_metadata(entry) patch = confined(directory, entry["patch"]) targets = entry["targets"] if not isinstance(targets, dict) or not targets: @@ -78,7 +158,7 @@ def validate_entry(directory, entry): raise ValueError("patch file has no valid targets") if set(targets) != {item["target"] for item in patches}: raise ValueError("target hashes must cover exactly the patch targets") - return patch, {name: digest.lower() for name, digest in targets.items()} + return patch, {name: digest.lower() for name, digest in targets.items()}, metadata def main(argv=None): @@ -126,7 +206,7 @@ def main(argv=None): for number, entry in entries: label = f"Catalog entry {number} ({entry.get('patch')!r})" try: - patch, targets = validate_entry(args.catalog.resolve(), entry) + patch, targets, catalog_metadata = validate_entry(args.catalog.resolve(), entry) except (OSError, ValueError, tomllib.TOMLDecodeError) as error: print(f"{label}: rejected: {error}") continue @@ -175,19 +255,32 @@ def main(argv=None): if verified[0] == 0 or verified[1] != verified[0] or verified[2] or verified[3]: print(f"{label}: rejected: relinker did not confirm every patch") continue - matches.append((number, entry, patch, targets)) + matches.append((number, entry, patch, targets, catalog_metadata)) + id_counts = Counter(catalog_metadata["id"] for _, _, _, _, catalog_metadata in matches if "id" in catalog_metadata) + if any(count > 1 for count in id_counts.values()): + unique = [] + for match in matches: + number, entry, patch, targets, catalog_metadata = match + identifier = catalog_metadata.get("id") + if identifier is not None and id_counts[identifier] > 1: + print(f"Catalog entry {number} ({entry['patch']!r}): rejected: duplicate verified id {identifier!r}") + else: + unique.append(match) + matches = unique if args.list_verified and matches: print(f"Verified catalog entries: {len(matches)}") print("Choose one patch file; listed entries are not combined.") - for number, entry, patch, targets in matches: + for number, entry, patch, targets, catalog_metadata in matches: print(f"Catalog entry {number}: {patch}") + print_metadata(catalog_metadata) for name, digest in targets.items(): print(f" {name} SHA-256 {digest}") print(f"Use these options when converting:\n {conversion_options(args, patch)}") return 0 if len(matches) == 1: - number, entry, patch, targets = matches[0] + number, entry, patch, targets, catalog_metadata = matches[0] print(f"Recommended: {patch} (catalog entry {number})") + print_metadata(catalog_metadata) for name, digest in targets.items(): print(f" {name} SHA-256 {digest}") print(f"Use these options when converting:\n {conversion_options(args, patch)}") diff --git a/tools/tests/test_suggest_patches.py b/tools/tests/test_suggest_patches.py index f24f84b86d..5215476c95 100644 --- a/tools/tests/test_suggest_patches.py +++ b/tools/tests/test_suggest_patches.py @@ -282,6 +282,87 @@ class PatchAdvisorTests(unittest.TestCase): self.assertIn("Choose one patch file; listed entries are not combined.", output.getvalue()) self.assertNotIn("Recommended:", output.getvalue()) + @patch.object(suggest_patches.subprocess, "run") + def test_optional_catalog_metadata_is_displayed_after_verification(self, run): + self.entry.update({"id": "missing_module_fix", "name": "Missing module fix", "category": "compatibility", + "description": "Removes an unused dependency", "notes": "Review before converting", + "contributors": ["Ada", "Ben"], "source": "https://example.org/patches/test00001", + "revision": 2, "tested": [{"stage": "verification", "result": "passed", + "evidence": "One synthetic patch verified", "environment": "Linux", + "date": "2026-10-26"}, + {"stage": "launch", "result": "partial", + "evidence": "Reached the title screen"}]}) + self.write_index() + run.return_value = subprocess.CompletedProcess([], 0, VERIFIED, "") + for options, heading in (((), "Recommended:"), (("--list-verified",), "Verified catalog entries: 1")): + with self.subTest(options=options): + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(self.advise(*options), 0) + self.assertIn(heading, output.getvalue()) + for detail in ("ID: missing_module_fix", "Name: Missing module fix", "Category: compatibility", + "Description: Removes an unused dependency", "Notes: Review before converting", + "Contributor: Ada", "Contributor: Ben", "Source: https://example.org/patches/test00001", + "Revision: 2", "Tested: verification passed on Linux (2026-10-26): One synthetic patch verified", + "Tested: launch partial: Reached the title screen"): + self.assertIn(detail, output.getvalue()) + self.assertEqual(run.call_count, 2) + self.module.write_bytes(b"different module") + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(self.advise("--list-verified"), 1) + self.assertNotIn("Name: Missing module fix", output.getvalue()) + self.assertEqual(run.call_count, 2) + + @patch.object(suggest_patches.subprocess, "run") + def test_invalid_optional_metadata_does_not_hide_valid_entry(self, run): + invalid = (("id", "Bad ID", "id must match"), ("name", "line\nbreak", "name must be"), + ("category", ["fps"], "invalid category"), ("description", 7, "description must be"), + ("tested", [{"stage": "gameplay", "result": "passed"}], "invalid tested record fields"), + ("tested", [{"stage": "gameplay", "result": "passed", "evidence": "ran", "date": "2026-02-30"}], "tested date must be a valid calendar date"), + ("notes", "", "notes must be"), ("contributors", ["Ada", "ada"], "duplicate contributor"), + ("source", "file:///tmp/patch", "source must be an HTTPS URL"), + ("revision", True, "revision must be a positive integer"), + ("unexpected", "value", "invalid fields")) + run.return_value = subprocess.CompletedProcess([], 0, VERIFIED, "") + for field, value, error in invalid: + with self.subTest(field=field, value=value): + bad = dict(self.entry, **{field: value}) + self.write_index([bad, self.entry]) + run.reset_mock() + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(self.advise("--list-verified"), 0) + self.assertIn(f"Catalog entry 1 ('patch.toml'): rejected: {error}", output.getvalue()) + self.assertIn("Verified catalog entries: 1", output.getvalue()) + self.assertIn(f"Catalog entry 2: {self.catalog / 'patch.toml'}", output.getvalue()) + run.assert_called_once() + + @patch.object(suggest_patches.subprocess, "run") + def test_duplicate_verified_ids_are_rejected_without_blocking_other_ids(self, run): + first = dict(self.entry, id="shared") + second = dict(self.entry, id="shared") + third = dict(self.entry, id="other") + self.write_index([first, second, third]) + run.return_value = subprocess.CompletedProcess([], 0, VERIFIED, "") + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(self.advise("--list-verified"), 0) + self.assertIn("Catalog entry 1 ('patch.toml'): rejected: duplicate verified id 'shared'", output.getvalue()) + self.assertIn("Catalog entry 2 ('patch.toml'): rejected: duplicate verified id 'shared'", output.getvalue()) + self.assertIn("Verified catalog entries: 1", output.getvalue()) + self.assertIn(f"Catalog entry 3: {self.catalog / 'patch.toml'}", output.getvalue()) + self.assertNotIn(f"Catalog entry 1: {self.catalog / 'patch.toml'}", output.getvalue()) + self.assertEqual(run.call_count, 3) + other_version = dict(second, contentVersion="02.000.000") + self.write_index([first, other_version]) + run.reset_mock() + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(self.advise(), 0) + self.assertIn("ID: shared", output.getvalue()) + run.assert_called_once() + @patch.object(suggest_patches.subprocess, "run") def test_list_verified_skips_individual_errors(self, run): bad = dict(self.entry, patch="missing.toml") From 6ad4710217ebc1180777dd1e39cd61cc3bb0c502 Mon Sep 17 00:00:00 2001 From: WhiteAssassins <28965087+WhiteAssassins@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:01:37 -0400 Subject: [PATCH 200/702] docs(tools): clarify patch catalog metadata compatibility --- docs/user/USAGE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 8c757cc18e..77cb16001d 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -124,7 +124,7 @@ The advisor reads `titleId` and `contentVersion` from the specified `param.json` } ``` -Catalog entries may also contain these optional fields. Catalogs without them keep the same behavior and `format: 1`. +Catalog entries may also contain these optional fields. Catalogs without them keep the same behavior and `format: 1`. Older advisor versions reject matching entries containing these fields; use an advisor version that supports them. | Field | Value | |-------|-------| @@ -138,6 +138,8 @@ Catalog entries may also contain these optional fields. Catalogs without them ke | `source` | HTTPS link to the patch's provenance. The advisor only displays it; it does not fetch it. | | `revision` | Positive integer revision of the patch definition, separate from the game's `contentVersion`. | +`category` describes the patch's area, not whether its effect is a fix or an enhancement; use `description` to explain the effect. `revision` does not select the newest entry automatically. To replace a patch definition with the same `id` for one title and content version, remove the old entry from the catalog. If both revisions verify, the advisor rejects both as duplicate IDs. + `name`, `description` and `notes` are nonempty single-line text when present. They describe the catalog entry as a whole; `name` and `description` inside a patch TOML's `[[patch]]` describe individual byte changes. Each `tested` record requires `stage` (`verification`, `conversion`, `launch` or `gameplay`), `result` (`passed`, `failed` or `partial`) and nonempty single-line `evidence`. Optional `environment` is single-line text, and optional `date` is a calendar date in `YYYY-MM-DD` form. For example: ```json From 76859ffeac489d9bdd70ebb08a069b0b2d3b11c3 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:31:04 +0000 Subject: [PATCH 201/702] feat(libSceNpUtility): declare 2 stubs with shadPS4 signatures They still throw through NotImplemented_nid_no_patch; only the parameter lists change. The only source is shadPS4's PS4 prototypes; nothing is measured on a PS5. --- core/libs/prx/libSceNpUtility/Export.cpp | 4 ++-- core/libs/prx/libSceNpUtility/include/Api.hpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index 364f0db20c..92061623e1 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -297,7 +297,7 @@ int APS5_VABI sceNpAppLaunchLinkIntInitialize() { int APS5_VABI sceNpBandwidthTestShutdownWithDetailedInfo() { return SCE_NP_ERROR_INVALID_ARGUMENT; } -int APS5_VABI sceNpLookupCreateTitleCtxA() { +int APS5_VABI sceNpLookupCreateTitleCtxA(int userId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -457,7 +457,7 @@ int APS5_VABI sceNpUtilityTerm() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpWordFilterCreateTitleCtxA() { +int APS5_VABI sceNpWordFilterCreateTitleCtxA(int userId) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpUtility/include/Api.hpp b/core/libs/prx/libSceNpUtility/include/Api.hpp index 0fe054556f..469ecd9005 100644 --- a/core/libs/prx/libSceNpUtility/include/Api.hpp +++ b/core/libs/prx/libSceNpUtility/include/Api.hpp @@ -60,7 +60,7 @@ int APS5_VABI sceNpAppLaunchLinkIntGetCompatibleTitleIdList(); int APS5_VABI sceNpAppLaunchLinkIntGetCompatibleTitleIdNum(); int APS5_VABI sceNpAppLaunchLinkIntInitialize(); int APS5_VABI sceNpBandwidthTestShutdownWithDetailedInfo(); -int APS5_VABI sceNpLookupCreateTitleCtxA(); +int APS5_VABI sceNpLookupCreateTitleCtxA(int userId); int APS5_VABI sceNpLookupNetAbortRequest(); int APS5_VABI sceNpLookupNetCensorComment(); int APS5_VABI sceNpLookupNetConvertJidToNpId(); @@ -100,6 +100,6 @@ int APS5_VABI sceNpTitleMetadataIntGetInfo(); int APS5_VABI sceNpTitleMetadataIntGetNpTitleId(); int APS5_VABI sceNpUtilityInit(); int APS5_VABI sceNpUtilityTerm(); -int APS5_VABI sceNpWordFilterCreateTitleCtxA(); +int APS5_VABI sceNpWordFilterCreateTitleCtxA(int userId); } From 6521fc2a221ce6fd08b03047af287cf329f8397b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:31:04 +0000 Subject: [PATCH 202/702] docs(libSceNpUtility): record the single-source shadPS4 signatures of the declared stubs --- docs/dev/debt/libSceNpUtility.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpUtility.md b/docs/dev/debt/libSceNpUtility.md index 95ec9ab84a..96d13c2cb6 100644 --- a/docs/dev/debt/libSceNpUtility.md +++ b/docs/dev/debt/libSceNpUtility.md @@ -1,11 +1,13 @@ # libSceNpUtility technical debt +- [sceNpLookupCreateTitleCtxA, sceNpWordFilterCreateTitleCtxA](../../../core/libs/prx/libSceNpUtility/Export.cpp) - throw through `NotImplemented_nid_no_patch`; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. ### Unknown function info - [sceNpAppInfoIntAbortRequest, sceNpAppInfoIntCheckAvailability, sceNpAppInfoIntCheckAvailabilityA, sceNpAppInfoIntCheckAvailabilityAll, sceNpAppInfoIntCheckAvailabilityAllA and sceNpAppInfoIntCheckServiceAvailability](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppInfoIntCheckServiceAvailabilityA, sceNpAppInfoIntCheckServiceAvailabilityAll, sceNpAppInfoIntCheckServiceAvailabilityAllA, sceNpAppInfoIntCreateRequest, sceNpAppInfoIntDestroyRequest and sceNpAppInfoIntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppInfoIntInitialize, sceNpAppLaunchLink2IntAbortRequest, sceNpAppLaunchLink2IntCreateRequest, sceNpAppLaunchLink2IntDestroyRequest, sceNpAppLaunchLink2IntFinalize and sceNpAppLaunchLink2IntGetCompatibleTitleIdList](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppLaunchLink2IntGetCompatibleTitleIdNum, sceNpAppLaunchLink2IntInitialize, sceNpAppLaunchLinkIntAbortRequest, sceNpAppLaunchLinkIntCreateRequest, sceNpAppLaunchLinkIntDestroyRequest and sceNpAppLaunchLinkIntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpLookupCreateAsyncRequest, sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppLaunchLinkIntGetCompatibleTitleIdList, sceNpAppLaunchLinkIntGetCompatibleTitleIdNum, sceNpAppLaunchLinkIntInitialize and sceNpBandwidthTestShutdownWithDetailedInfo](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpLookupCreateTitleCtxA, sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpLookupNetConvertJidToNpId, sceNpLookupNetConvertNpIdToJid, sceNpLookupNetCreateRequest, sceNpLookupNetCreateTitleCtx, sceNpLookupNetDeleteRequest, sceNpLookupNetDeleteTitleCtx and sceNpLookupNetInit](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. @@ -14,7 +16,7 @@ - [sceNpServiceChecker2IntGetServiceAvailability, sceNpServiceChecker2IntGetServiceAvailabilityA, sceNpServiceChecker2IntInitialize, sceNpServiceChecker2IntIsSetServiceType and sceNpServiceCheckerIntAbortRequest](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpServiceCheckerIntCreateRequest, sceNpServiceCheckerIntDestroyRequest, sceNpServiceCheckerIntFinalize, sceNpServiceCheckerIntGetAvailability, sceNpServiceCheckerIntGetAvailabilityList and sceNpServiceCheckerIntInitialize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpServiceCheckerIntIsCached, sceNpTitleMetadataIntAbortRequest, sceNpTitleMetadataIntCreateRequest, sceNpTitleMetadataIntDeleteRequest, sceNpTitleMetadataIntGetInfo, sceNpTitleMetadataIntGetNpTitleId and sceNpUtilityInit](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpUtilityTerm and sceNpWordFilterCreateTitleCtxA](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpUtilityTerm](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. ### Functional From c19c8658dc8e4054a57a2693cec98b6bf68c0b9b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:50 +0000 Subject: [PATCH 203/702] docs(usage): list the partial class of the import audit --- docs/user/USAGE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index d10adeb020..e3b301807b 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -447,6 +447,7 @@ python3 tools/import_audit.py app.registry.json app.libc.prx.guest.prx.registry. | Class | Meaning | |-------|---------| | `implemented` | A built library exports the NID and its function is not a throwing stub. | +| `partial` | Exported, but the function throws for inputs whose behaviour is unknown. | | `stub` | Exported, but the whole function calls `NotImplemented_nid_no_patch`: the game loads and throws when it calls it. | | `absent` | No built library exports the NID: the loader fails. | | `module` | The import names a file found in `--modules` (the title's own library), which is trusted to provide it. | From ef824756df5475b7f63dfdfae09166800a39eb65 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:50 +0000 Subject: [PATCH 204/702] docs(usage): list every condition that makes the import audit exit 1 --- 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 e3b301807b..c34558b385 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -474,7 +474,7 @@ The audit also walks the built libraries' own host dependencies: each `.prx` und Registry `nid` fields may contain either encoded NIDs or ordinary ELF symbol names such as `strcmp`. Imports are matched against literal exports first, then against their computed NIDs when the literal lookup misses. Reports retain the original import name. Export availability does not establish that every platform loader supports that lookup path. -Exit codes: `0` nothing blocks loading, `1` there are `absent` imports or needed libraries without a file, `2` an input could not be read; the message names the file and the value. +Exit codes: `0` nothing blocks loading, `1` there are `absent` imports, import name mismatches, needed libraries or library host dependencies without a file, or, with `--strict-stubs` or `--strict-providers`, `stub` imports or imports exported only by another library, `2` an input could not be read; the message names the file and the value. A module registry lists `nid`, `library` and `targetOffset` for each relocation against a function that no other bundled module exports, without the `#` suffix of the executable's NIDs. From d09ff3989e82b9372d2378c31bd7c9433ac80d2b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:50 +0000 Subject: [PATCH 205/702] docs(game-testing): describe APS5_HANG_DUMP_SECS as a timed dump --- docs/dev/GAME_TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index de2c53b6f3..3318a3422d 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -79,7 +79,7 @@ cmd /c '.\app.exe > run.log 2>&1' - **Stall:** no `[present]` lines for a long time while other periodic lines keep coming. Some titles present only when the view changes, so first send one input (`tools/pad_input.py`) and check whether presents resume. - **Slowdown:** presents per 10 s far below earlier runs of the same scene. Record the scene (save file or level), since it changes the numbers, and check power and caches first. - **Crash on Linux:** take the `ip` from `dmesg` and its offset in the image. Repeat the run under `gdb` (launched by gdb, not attached) for registers, the faulting instructions and all thread stacks. -- **Crash on Windows:** the `FATAL: unhandled exception ...` report in the log lists the faulting `rip` (for example `app.exe+0x...`), the registers and a stack scan. `APS5_CRASH_INDIRECT_MEMORY=1` adds up to 64 bytes from each pointer in the register dump. `APS5_HANG_DUMP_SECS=` writes a dump after a hang of n seconds. `APS5_TRACE_EXIT=1` reports the process exit point. +- **Crash on Windows:** the `FATAL: unhandled exception ...` report in the log lists the faulting `rip` (for example `app.exe+0x...`), the registers and a stack scan. `APS5_CRASH_INDIRECT_MEMORY=1` adds up to 64 bytes from each pointer in the register dump. `APS5_HANG_DUMP_SECS=` (Windows) prints the crash report with every thread n seconds after start-up and then aborts the title, so set it to a time after the title has stopped. `APS5_TRACE_EXIT=1` reports the process exit point. - **Traces (heavy):** `APS5_TRACE_FRAMES=:` or `APS5_TRACE_SECONDS=:` writes a capture for `tools/trace_digest.py`. Traces slow the title and can move the crash; say so when reporting. ## Measuring performance From 1bb9358152ac16bafbd6e3ef3adad2cafdb88ba4 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:50 +0000 Subject: [PATCH 206/702] docs(game-testing): link the MinGW DLL note to the runtime layout section --- docs/dev/GAME_TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index 3318a3422d..ade7d2a3b7 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -13,7 +13,7 @@ For user options see [USAGE](../user/USAGE.md); for internals see [ARCHITECTURE] - Windows: `relinker --windows --registry src/eboot.elf app.exe`. Without `--windows` the output is an ELF file whatever its name, and Windows refuses to start it (exit code 216, "not compatible with the version of Windows"). - A dump whose `app0` holds `ampr_assets.index` and `ampr_assets-*.pak` was repacked for [ampr_emu](https://github.com/drakmor/ampr_emu): the packed files were removed from `app0`, and only ampr_emu reads the packs. Restore them first with ampr_emu's `tools/ampr_pack.py unpack --index app0/ampr_assets.index --output ` and copy the files missing from `app0`. Without them a Unity title stops at start-up on the missing `Media/Resources/unity default resources`. - Convert every module the title loads from its own folders (`sce_module/`, `Media/Modules/`, `Media/Plugins/`). Unity titles need `--module-dir Media/Modules --module-dir Media/Plugins`. The relinker writes each converted module as `.guest.prx` into `app0/` beside the output, in the module's own folder, and lists it in the `Arrange the files like this` tree it prints. Keep the original `.prx` next to it. -- `libs/` holds only AnyPS5's own libraries, from `build/core/libs/libs` (not `unpatched/`). A game module placed there fails to load (error 193 on Windows). A self-built `libs/` on Windows also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the toolchain's `mingw64/bin`, or every library fails with error 126 ([USAGE](../user/USAGE.md#input-and-conversion)). +- `libs/` holds only AnyPS5's own libraries, from `build/core/libs/libs` (not `unpatched/`). A game module placed there fails to load (error 193 on Windows). A self-built `libs/` on Windows also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the toolchain's `mingw64/bin`, or every library fails with error 126 ([USAGE](../user/USAGE.md#runtime-layout)). - Run `tools/import_audit.py` on the registries. Exit code 1 lists imports that will fail or throw. `tools/audit_dump.py --relinker --libs build/core/libs/libs` (`--windows` on Windows) does the unwrapping, the conversion with `--registry` and the audit in one step, from every module folder above and `.prx` From 09bd0731fc3a9db151d2ca7110bbcfdd38fd69d4 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:50 +0000 Subject: [PATCH 207/702] docs(game-testing): say what exit code 1 of the import audit means --- docs/dev/GAME_TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index ade7d2a3b7..5a3ab4513d 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -14,7 +14,7 @@ For user options see [USAGE](../user/USAGE.md); for internals see [ARCHITECTURE] - A dump whose `app0` holds `ampr_assets.index` and `ampr_assets-*.pak` was repacked for [ampr_emu](https://github.com/drakmor/ampr_emu): the packed files were removed from `app0`, and only ampr_emu reads the packs. Restore them first with ampr_emu's `tools/ampr_pack.py unpack --index app0/ampr_assets.index --output ` and copy the files missing from `app0`. Without them a Unity title stops at start-up on the missing `Media/Resources/unity default resources`. - Convert every module the title loads from its own folders (`sce_module/`, `Media/Modules/`, `Media/Plugins/`). Unity titles need `--module-dir Media/Modules --module-dir Media/Plugins`. The relinker writes each converted module as `.guest.prx` into `app0/` beside the output, in the module's own folder, and lists it in the `Arrange the files like this` tree it prints. Keep the original `.prx` next to it. - `libs/` holds only AnyPS5's own libraries, from `build/core/libs/libs` (not `unpatched/`). A game module placed there fails to load (error 193 on Windows). A self-built `libs/` on Windows also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the toolchain's `mingw64/bin`, or every library fails with error 126 ([USAGE](../user/USAGE.md#runtime-layout)). -- Run `tools/import_audit.py` on the registries. Exit code 1 lists imports that will fail or throw. +- Run `tools/import_audit.py` on the registries. Exit code 1 means an import or a needed library is missing; the report also lists `stub` imports, which throw when called, and `--strict-stubs` makes them fail the audit too. `tools/audit_dump.py --relinker --libs build/core/libs/libs` (`--windows` on Windows) does the unwrapping, the conversion with `--registry` and the audit in one step, from every module folder above and `.prx` files beside `eboot.bin`, into a temporary directory (or `--work `), reading the dump only. From cc2dc0a9520351eaeb3d72b59cda052c424b52ef Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:50 +0000 Subject: [PATCH 208/702] docs(input): note that the example's ToggleMouse line replaces F7 --- docs/user/INPUT_MAPPING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 05c49f2559..eed7bfb087 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -45,7 +45,7 @@ Without an `anyps5-input.ini`, or for actions the file does not mention, these b | `TogglePerformanceOverlay` | F10 | | `Screenshot` | F12 | -For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: +For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, makes the middle mouse button the only `ToggleMouse` input (the line replaces F7), and keeps all other built-in bindings: ```ini Cross = KEY:F From 0ccda196c9770afcd06f4e935182992b8ee24bf5 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:02:50 +0000 Subject: [PATCH 209/702] docs(input): say that an invalid mapping file stops the game --- docs/user/INPUT_MAPPING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index eed7bfb087..69236383aa 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -130,7 +130,7 @@ MouseCurve = 1.3 Earlier versions sampled the mouse every 33 ms and tilted the stick at least halfway for any movement. `MouseDeadzoneOffset = 0.5`, `MouseSmoothingMs = 0` and `MouseDecayMs = 33` come close to that behavior. -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. +An invalid line stops the game when video output starts, with an error that names the file and line number. If the file named by `ANYPS5_INPUT_CONFIG` does not exist, or the configuration file cannot be read, the game stops with an error. With no `anyps5-input.ini` and no `ANYPS5_INPUT_CONFIG`, the built-in mapping is used. ## Web configurator From 57b58fabf0fbf453f8cc584862719a628fd8edc9 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:16:27 +0000 Subject: [PATCH 210/702] docs(usage): describe the param.sfo fallback for title metadata --- 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 c34558b385..80bd82380f 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -203,7 +203,7 @@ app0/ Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/` and fail to load their fonts without it). Only the converted executable runs. On Linux, a symbolic link inside the `app0/` tree is listed with the type of the file or directory it points to, so a linked directory is walked like a real one. Links inside a linked directory whose target is outside `app0/` keep the link type; for nested trees, link only the top-level entries or use copies or hard links. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. -Some applications also need their original `app0/sce_sys/param.json` at runtime. AnyPS5 reads this file for the title ID and display name; it must contain a non-empty `titleId` string and a non-empty `localizedParameters` object with a language entry that has a non-empty `titleName`. It selects the entry named by `defaultLanguage` (inside `localizedParameters`, or at the top level); if that entry is absent, it tries `en-US`, then the first object-valued language entry. Preserve the values from the title's own metadata and place the file under the same `app0/` directory as the other app resources. At startup, its presence in the working directory creates `data/`; a non-zero `downloadDataSize` also creates `download0/`. Without the file, neither directory is created. If it is missing or invalid, metadata calls such as title/window setup can fail with a `param.json` error. +Some applications also need their original `app0/sce_sys/param.json` at runtime. AnyPS5 reads this file for the title ID and display name; it must contain a non-empty `titleId` string and a non-empty `localizedParameters` object with a language entry that has a non-empty `titleName`. It selects the entry named by `defaultLanguage` (inside `localizedParameters`, or at the top level); if that entry is absent, it tries `en-US`, then the first object-valued language entry. Preserve the values from the title's own metadata and place the file under the same `app0/` directory as the other app resources. At startup, its presence in the working directory creates `data/`; a non-zero `downloadDataSize` also creates `download0/`. Without `param.json`, AnyPS5 reads `app0/sce_sys/param.sfo` (`TITLE`, `TITLE_ID` and `DOWNLOAD_DATA_SIZE`) instead, with the same effect on `data/` and `download0/`; without either file, neither directory is created. If both are missing or the one read is invalid, metadata calls such as title/window setup can fail with a `param.json` or `param.sfo` error. Resources can also be packed into `app0.zar` beside `app0/`, a [ZArchive](https://github.com/Exzap/ZArchive) whose root is the `app0/` directory. A file present in `app0/` is read from there and any other file from the archive; the executable, `sce_module/` and `sce_sys/` stay in `app0/`. The archive is read only through `sceKernelOpen`/`open`, the read, seek and stat calls on its descriptors and `sceKernelStat`; other calls on an archived path, listing a directory that the archive also contains and opening a file in it for writing fail as described in [technical debt](../dev/debt/libkernel.md). An archive that cannot be opened stops the title at its first file access. From e74c19d2c6b8ad1fcf5757d056e38b53fd8dc778 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:16:27 +0000 Subject: [PATCH 211/702] docs(usage): say that the reward icon call returns an error instead of stopping --- 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 80bd82380f..6e70b3d950 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -280,7 +280,7 @@ Many games unlock trophies through statistics instead: `app0/sce_sys/uds/udsNN.u `APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, every statistic an event sets, such as `[uds] stat 10 of user 268435456 is 10 (COLLECT $.counter)`, and a `[uds]` line for a missing `trophyNN.ucp` or `udsNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist; and, once per rule, an event whose property is missing, negative, not an integer or outside the statistic's range. -A missing or malformed `trophyNN.ucp` or `udsNN.ucp` turns off only what it provides, and a malformed one prints the reason once: without trophy data the queries report placeholders and statistics unlock nothing, and without statistics rules events change no statistic. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon`, `sceNpTrophy2GetTrophyIcon` and `sceNpTrophy2GetRewardIcon` stop the game with an error. So does an unlock or statistics file in `_trophy/` that is damaged (a line that is not a new `<id> <value>` pair, such as a line an edit left blank or ended with a carriage return) or cannot be read or written. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. +A missing or malformed `trophyNN.ucp` or `udsNN.ucp` turns off only what it provides, and a malformed one prints the reason once: without trophy data the queries report placeholders and statistics unlock nothing, and without statistics rules events change no statistic. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon` and `sceNpTrophy2GetTrophyIcon` stop the game with an error, and `sceNpTrophy2GetRewardIcon` returns `SCE_NP_TROPHY2_ERROR_ICON_FILE_NOT_FOUND`. So does an unlock or statistics file in `_trophy/` that is damaged (a line that is not a new `<id> <value>` pair, such as a line an edit left blank or ended with a carriage return) or cannot be read or written. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. ### PlayGo dumps without metadata From 04ebed409026a0783ba3a3037ce89490f327303d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:16:28 +0000 Subject: [PATCH 212/702] docs(usage): update the storage texture cache cap --- 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 6e70b3d950..e774fab438 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -334,7 +334,7 @@ Besides the recompiled shaders, the shader cache directory holds `pipelines.jour Image and buffer memory allocations retry after releasing unused pooled buffers and reclaimable texture cache entries. Recovery preserves storage images with pending GPU results and resources retained by recorded work. `[bufferpool]` recovery and failure lines report the requested allocation, memory released and retry count. An allocation still fails if recovery cannot free enough memory. -Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. As the heap's other use grows the sampled budget falls first, to that floor, then the storage budget; the storage budget also gives back what the sampled textures alive take as they grow. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=<n>` fixes both budgets at `n` MiB, and the lines are then not printed. +Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves and an eighth of the budget; it is capped at half of that budget or half of the heap's size, whichever is smaller, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. As the heap's other use grows the sampled budget falls first, to that floor, then the storage budget; the storage budget also gives back what the sampled textures alive take as they grow. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=<n>` fixes both budgets at `n` MiB, and the lines are then not printed. A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget was 1797 MiB under the earlier rule, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. From aa461c121947c6ad13bb0113c8232e55d7a51745 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:16:28 +0000 Subject: [PATCH 213/702] docs(usage): note that ANYPS5_OVERLAY=0 is accepted --- 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 e774fab438..781e5a6e3d 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -372,7 +372,7 @@ Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/ ### Performance overlay -F10 cycles a performance overlay over the presented image through three states: off, page 1 (the table below), page 2 (a detailed page, the second table), then off again. `ANYPS5_OVERLAY=1` shows page 1 from the first frame and `ANYPS5_OVERLAY=2` page 2; F10 then goes on from there. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. +F10 cycles a performance overlay over the presented image through three states: off, page 1 (the table below), page 2 (a detailed page, the second table), then off again. `ANYPS5_OVERLAY=1` shows page 1 from the first frame and `ANYPS5_OVERLAY=2` page 2; F10 then goes on from there. `ANYPS5_OVERLAY=0` is the same as unset. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. | Line | Meaning | |------|---------| From 5dfc20dcf27734c0c3146f457ada517885e7933e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:16:28 +0000 Subject: [PATCH 214/702] docs(usage): link late storage image stores to the AGC driver debt file --- 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 781e5a6e3d..1db5820a31 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -340,7 +340,7 @@ A fixed budget above the automatic one can exceed what the card holds next to th ### Storing GPU results at each flip -GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. +GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([technical debt](../dev/debt/libSceAgcDriver.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. A title's own CPU reads of that memory are not seen, so a title that waits for a GPU label and then reads the results reads stale memory. `APS5_STORE_AT_LABEL=1` stores every pending result before each label the GPU writes (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`), at the cost of a write-back of every pending image per label. Any other value stops the title at its first label. From 3d2aebfa0313a518a43530f2a9da777f196cb330 Mon Sep 17 00:00:00 2001 From: BunnyFl <166477956+BunnyFl@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:12:56 +0100 Subject: [PATCH 215/702] fix(tools): keep nested preprocessor branches lazy in import_audit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- tools/import_audit.py | 38 +++++++++++++++++++++----------- tools/tests/test_import_audit.py | 22 ++++++++++-------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index ef0d726615..663111dde9 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -1,6 +1,5 @@ import argparse import hashlib -import itertools import json import os import re @@ -536,22 +535,35 @@ def branch_variants(body): raise AuditError("#if without a matching #endif") root.append(body[position:]) + def choices(item): + if isinstance(item, str): + return iter((item,)) + return (choice for alternative in item["alternatives"] for choice in expand(alternative)) + def expand(items): - parts = [] - for item in items: - if isinstance(item, str): - parts.append((item,)) - continue - choices = [choice for alternative in item["alternatives"] for choice in expand(alternative)] - if len(choices) > BRANCH_VARIANT_LIMIT: - raise AuditError(f"more than {BRANCH_VARIANT_LIMIT} preprocessor branch combinations in one function") - parts.append(choices) - for count, variant in enumerate(itertools.product(*parts), 1): + iterators = [choices(item) for item in items] + values = [next(iterator) for iterator in iterators] + while True: + yield "".join(values) + index = len(items) - 1 + while index >= 0: + try: + values[index] = next(iterators[index]) + break + except StopIteration: + iterators[index] = choices(items[index]) + values[index] = next(iterators[index]) + index -= 1 + if index < 0: + return + + def limited(variants): + for count, variant in enumerate(variants, 1): if count > BRANCH_VARIANT_LIMIT: raise AuditError(f"more than {BRANCH_VARIANT_LIMIT} preprocessor branch combinations in one function") - yield "".join(variant) + yield variant - return expand(root) + return limited(expand(root)) def variant_calls(body): diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 4be39dc364..93d2e30148 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1608,16 +1608,20 @@ class ImportAuditTests(unittest.TestCase): "void Fail() { NotImplemented_nid_no_patch(__func__); }\n" "int APS5_VABI sceStub(int value) { NotImplemented_nid_no_patch(__func__); return 0; }\n" f"int APS5_VABI sceHosts(int value) {{\n{blocks} return value;\n}}\n" - f"int APS5_VABI sceLastHost(int value) {{\n{blocks}#ifdef HOST_LAST\n Fail();\n#endif\n return value;\n}}\n") + f"int APS5_VABI sceLastHost(int value) {{\n{blocks}#ifdef HOST_LAST\n Fail();\n#endif\n return value;\n}}\n" + f"int APS5_VABI sceNestedHosts(int value) {{\n#ifdef HOST_OUTER\n{blocks}#else\n Fail();\n#endif\n return value;\n}}\n") stubs, partial = import_audit.source_stub_classes(source) - stub, hosts, last = (("libHosts.prx", nid_names.compute_nid(name)) for name in ("sceStub", "sceHosts", "sceLastHost")) - self.assertEqual(set(stubs), {stub, last}) - self.assertEqual(partial, {last}) - write_source(source, "libHosts", - "void Fail() { NotImplemented_nid_no_patch(__func__); }\n" - f"int APS5_VABI sceEveryHost(int value) {{\n{blocks} Fail();\n return value;\n}}\n") - with self.assertRaisesRegex(import_audit.AuditError, "more than 256 preprocessor branch combinations"): - import_audit.source_stub_classes(source) + stub, hosts, last, nested = (("libHosts.prx", nid_names.compute_nid(name)) + for name in ("sceStub", "sceHosts", "sceLastHost", "sceNestedHosts")) + self.assertEqual(set(stubs), {stub, last, nested}) + self.assertEqual(partial, {last, nested}) + for every in (f"{blocks} Fail();\n", f"#ifdef HOST_OUTER\n{blocks}#endif\n Fail();\n"): + with self.subTest(every=every): + write_source(source, "libHosts", + "void Fail() { NotImplemented_nid_no_patch(__func__); }\n" + f"int APS5_VABI sceEveryHost(int value) {{\n{every} return value;\n}}\n") + with self.assertRaisesRegex(import_audit.AuditError, "more than 256 preprocessor branch combinations"): + import_audit.source_stub_classes(source) def test_partial_import_is_its_own_class(self): direct, guarded = nid_names.compute_nid("sceDirect"), nid_names.compute_nid("sceGuarded") From a2b59db71a6794db31797ed5ed67afcc82083460 Mon Sep 17 00:00:00 2001 From: B-Howell <116104752+B-Howell@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:19:37 -0400 Subject: [PATCH 216/702] feat(libc): implement strtoq Add the BSD "quad" `strtoq` export for the libc PRX as a thin wrapper around `strtoll`, as FreeBSD 11.4 lib/libc/stdlib/strtoq.c does. The unsigned sibling `strtouq` is already here as the same alias of `strtoull`. Extend the guest wide strings test to probe the signed path on an under/overflow run and a short positive run with a trailing non-digit. Document the export in docs/dev/debt/libc.md with the registry NID `yUUOxgk8Dmc`; console behavior is unverified beyond the existing `strtoll` contract. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/Strings.cpp | 4 ++++ core/libs/tests/GuestWideStrings.cpp | 4 ++++ docs/dev/debt/libc.md | 2 ++ 4 files changed, 11 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 441d680159..21f5f91b8d 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -954,6 +954,7 @@ std::int64_t APS5_VABI strtol_nid_postfix(const char* str, char** endptr, int ba std::uint64_t APS5_VABI strtoul_nid_postfix(const char* str, char** endptr, int base); long long APS5_VABI strtoll_nid_postfix(const char* str, char** endptr, int base); unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr, int base); +long long APS5_VABI strtoq_nid_postfix(const char* str, char** endptr, int base); unsigned long long APS5_VABI strtouq_nid_postfix(const char* str, char** endptr, int base); double APS5_VABI strtod_nid_postfix(const char* str, char** endptr); double APS5_VABI atof_nid_postfix(const char* str); diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 508da2aa04..ecea54b9f1 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -399,6 +399,10 @@ unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr return ParseInteger<unsigned long long>(str, endptr, base, std::strtoull); } +long long APS5_VABI strtoq_nid_postfix(const char* str, char** endptr, int base) { + return strtoll_nid_postfix(str, endptr, base); +} + unsigned long long APS5_VABI strtouq_nid_postfix(const char* str, char** endptr, int base) { return strtoull_nid_postfix(str, endptr, base); } diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index db3553fee9..958831ce25 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -397,6 +397,10 @@ int main() { char* narrowEnd = nullptr; const char text[] = "0x1ffffffffffffffffg"; require(strtouq_nid_postfix(text, &narrowEnd, 16) == UINT64_MAX && narrowEnd == text + 19); + const char signedText[] = " -0x7fffffffffffffff9"; + require(strtoq_nid_postfix(signedText, &narrowEnd, 0) == INT64_MIN && narrowEnd == signedText + 22); + const char positiveText[] = "+42z"; + require(strtoq_nid_postfix(positiveText, &narrowEnd, 10) == 42 && narrowEnd == positiveText + 3); const char path[] = "a/b/c"; require(rindex_nid_postfix(path, '/') == path + 3 && rindex_nid_postfix(path, 'x') == nullptr); require(rindex_nid_postfix(path, 0) == path + 5); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..364dac057f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -74,6 +74,8 @@ - [fpurge](../../../core/libs/prx/libc/src/StreamPurge.cpp) follows FreeBSD 11.4 [fpurge.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/stdio/fpurge.c): discard buffered input, pushback and output without writing or seeking, preserving EOF and error status, and report `EBADF` for a closed stream. Console behavior is unverified. The Windows path resets the existing host UCRT buffer representation; Linux and macOS use their native purge operations. Null streams throw as existing stream wrappers do. +- [strtoq](../../../core/libs/prx/libc/src/Strings.cpp) (libc, NID `yUUOxgk8Dmc`) is the BSD "quad" alias of `strtoll` and forwards to it, as FreeBSD 11.4 [strtoq.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtoq.c) does. The console export is not verified; the sibling `strtouq` is already here. + ### Functional - [_Cosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the two-argument Dinkumware CRT helper used by `std::complex`, including a scaled large-finite-input path; the signature, exact result and floating-point exception behavior are not verified against the console. From 1add0a58590a84b7ead05e5ee6d3ad61e8fb3732 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Thu, 22 Oct 2026 02:01:09 -0400 Subject: [PATCH 217/702] fix(libc): follow the console's rand, srand and rand_r rand and rand_r both used FreeBSD's do_rand (Park and Miller on state % 0x7ffffffe + 1, minus one), so rand returned values up to 0x7ffffffd and rand(), rand_r() and every seeded sequence differed from the console. The libc.prx that titles ship (the same code in the copies of PPSA13934, PPSA07923 and PPSA05916, read from disassembly) uses two different generators: - rand steps a 64-bit state, initially 1, as state * 0x5851F42D4C957F2D + 1 and returns (state >> 32) & 0x3FFFFFFF, so its results stay at or below 0x3FFFFFFF. srand stores the seed zero-extended as that state. - rand_r is the minimal standard generator, state * 16807 modulo 2^31 - 1 on the signed 32-bit state (1 gives 16807, 282475249, 1622650073), and a zero state becomes 0x2AE1B767. guest_math now expects those results: the rand_r chain from 1, the zero and 0x7FFFFFFF states, rand after srand(1), srand(0) and srand(0xFFFFFFFF), and rand's range over 1000 calls. Its previous checks expected FreeBSD's values and rand matching rand_r. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Math.cpp | 25 +++++++++++++------------ core/libs/tests/GuestMath.cpp | 24 ++++++++++++++++-------- docs/dev/debt/libc.md | 1 + 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 305b915f41..073bcf950e 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1482,27 +1482,28 @@ DinkumwareFloatCharacteristics _Ldbl_nid_postfix {18, 64, 4932, 16384, -4931, -1 {0, 0, 0, 0x8000, 0x3fc0}, {0xffff, 0xffff, 0xffff, 0xffff, 0x7ffe}, {0, 0, 0, 0x8000, 0x0001}}; static std::mutex g_randLock; -static std::uint32_t g_randState = 1; +static std::uint64_t g_randState = 1; -static int NextRand_nid_no_patch(std::uint32_t& state) { - const std::int64_t x = static_cast<std::int64_t>(state % 0x7ffffffeu) + 1; - std::int64_t next = 16807 * (x % 127773) - 2836 * (x / 127773); - if (next < 0) next += 0x7fffffff; - state = static_cast<std::uint32_t>(next - 1); - return static_cast<int>(next - 1); +static std::uint32_t MinimalStandardNext(std::int32_t state) { + if (state == 0) return 0x2ae1b767u; + const auto product = static_cast<std::uint64_t>(static_cast<std::int64_t>(state)) * 16807u; + const auto high = static_cast<std::uint32_t>(product >> 31u); + const auto sum = (static_cast<std::uint32_t>(product) & 0x7fffffffu) + high; + if (sum < 0x7fffffffu) return sum; + if (sum == 0x7fffffffu) return 0; + return static_cast<std::int32_t>(high) <= 0 ? (sum & 0x7fffffffu) - 1u : (sum & 0x7fffffffu) + 1u; } int APS5_VABI rand_nid_postfix() { std::lock_guard lock(g_randLock); - return NextRand_nid_no_patch(g_randState); + g_randState = g_randState * 0x5851f42d4c957f2dull + 1u; + return static_cast<int>((g_randState >> 32u) & 0x3fffffffu); } int APS5_VABI rand_r_nid_postfix(unsigned int* state) { if (state == nullptr) throw std::invalid_argument("rand_r: null state"); - std::uint32_t value = *state; - const int result = NextRand_nid_no_patch(value); - *state = value; - return result; + *state = MinimalStandardNext(static_cast<std::int32_t>(*state)); + return static_cast<int>(*state); } void APS5_VABI srand_nid_postfix(unsigned int seed) { diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index ea2219eb63..6eb57f16e6 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -1790,15 +1790,23 @@ int main() { Require(std::abs(atan2f_nid_postfix(1.f, -1.f) - 2.3561945f) < 0.000001f); Require(tanf_nid_postfix(0.f) == 0.f); unsigned int randomState = 1; - Require(rand_r_nid_postfix(&randomState) == 33613 && randomState == 33613u); + Require(rand_r_nid_postfix(&randomState) == 16807 && randomState == 16807u); + Require(rand_r_nid_postfix(&randomState) == 282475249 && rand_r_nid_postfix(&randomState) == 1622650073); randomState = 0; - Require(rand_r_nid_postfix(&randomState) == 16806 && randomState == 16806u); - for (const unsigned int seed : {0u, 1u, 12345u, 0x7ffffffeu, 0xffffffffu}) { - srand_nid_postfix(seed); - - randomState = seed; - for (int step = 0; step < 8; ++step) Require(rand_r_nid_postfix(&randomState) == rand_nid_postfix()); - } + Require(rand_r_nid_postfix(&randomState) == 0x2ae1b767 && randomState == 0x2ae1b767u); + randomState = 0x7fffffffu; + Require(rand_r_nid_postfix(&randomState) == 0 && randomState == 0u); + randomState = 0x7ffffffeu; + Require(rand_r_nid_postfix(&randomState) == 2147466840 && randomState == 2147466840u); + randomState = 0xffffffffu; + Require(rand_r_nid_postfix(&randomState) == 2147466840 && randomState == 2147466840u); + srand_nid_postfix(1); + Require(rand_nid_postfix() == 408024109 && rand_nid_postfix() == 11635919 && rand_nid_postfix() == 196474438); + srand_nid_postfix(0); + Require(rand_nid_postfix() == 0 && rand_nid_postfix() == 408024109); + srand_nid_postfix(0xffffffffu); + Require(rand_nid_postfix() == 876841727 && rand_nid_postfix() == 830605684); + for (int step = 0; step < 1000; ++step) Require(static_cast<unsigned int>(rand_nid_postfix()) <= 0x3fffffffu); bool threw = false; try { rand_r_nid_postfix(nullptr); } catch (...) { threw = true; } Require(threw); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..9aea7ccff0 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -47,6 +47,7 @@ - [feraiseexcept](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc, NID `NIfFNcyeCTo`) - console behavior and module/library versions have not been measured. The signature follows the [public PS5 SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/fenv.h); flag updates and x87 trap delivery follow [FreeBSD amd64 fenv.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/msun/amd64/fenv.c). All six defined exception bits are supported. Unlike the FreeBSD reference, which does not mask or validate the requested bits, this implementation rejects other bits with `std::invalid_argument` before modifying state; console behavior for those bits is unknown. This is distinct from `_Feraise`, which also modifies errno and adds inexact for range exceptions. - [_Mtx_timedlock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `hPzYSd5Nasc`) follows the console's `libc.prx` at `0x5d50` (PPSA02530): it takes an absolute xtime and returns `_Thrd_timedout` (2) on expiry (`0x8002003c`) or 5 on other failures. On non-Windows hosts the native mutex and condition are `std::timed_mutex` and `std::condition_variable_any`; Windows keeps the `CRITICAL_SECTION`/`CONDITION_VARIABLE` pair because `std::condition_variable_any` failed `guest_libc_cnd_timed_wait` on the Windows runner, and its `_Mtx_timedlock` polls `TryEnterCriticalSection` until the deadline. Windows timing is not measured on the console. - [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm +- [rand, srand and rand_r](../../../core/libs/prx/libc/src/Math.cpp) (libc) follow the `libc.prx` that titles ship (read from disassembly of the copies of PPSA13934, PPSA07923 and PPSA05916, not measured on a console): `rand` steps a 64-bit state, initially 1, as `state * 0x5851F42D4C957F2D + 1` and returns bits 32 to 61, so its results stay at or below `0x3FFFFFFF`; `srand` stores its seed zero-extended as that state; `rand_r` is the minimal standard generator, `state * 16807` modulo 2^31 - 1 on the signed 32-bit state, with a zero state becoming `0x2AE1B767`. The console's `rand` keeps its state without a lock; this one takes `g_randLock`. The console's system libc, which code without a bundled `libc.prx` uses, has a different generator (reported in #9648 from a PS4 Pro); it is not modelled. - [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) - a null state throws where the system `libSceLibcInternal` of PS5 firmware 13.60 faults, `initstate` below 8 bytes skips that library's "not enough state" warning, and concurrent callers (one lock here) and a title's own libc are not measured. Otherwise it follows payload processes on PS5 firmware 12.70 and 13.60: a fresh process draws the `srandom(1)` sequence; `srandom` seeds through `good_rand` (int32 argument, 0 becomes 123459876, `16807 * (x % 127773) - 2836 * (x / 127773)` plus 0x7fffffff when negative); `initstate` picks types 0 to 4 by size, returns null below 8 bytes without touching `errno` but still saves the current rear position; both calls return the previous state; `setstate` takes a rear position past the degree unchecked - [srandomdev](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) is not verified against the console. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NID `a2MOZf++Wjg`; it follows FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c), filling every active generator word from host OS entropy instead of `KERN_ARND`, then resetting additive feedback positions without warming up. An entropy failure throws rather than leaving `sysctl`'s failure unchecked. Calls share the generator lock. - [Integer compiler-rt builtins](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) (libc) - the console traps in `__divsi3`, `__modsi3` and the 64- and 128-bit divisions on a zero divisor (`div`) and in `__negv*`, `__absv*` and `__mulv*` on overflow (an abort helper, then `ud2`); these throw instead. As in the `libc.prx` bundled with PPSA08427 (disassembly), `__addv*` and `__subv*` wrap without an overflow check, and `__udivsi3`, `__umodsi3` and `__udivmodsi4` divide in software, giving a quotient of 0 and the dividend as remainder for a zero divisor; `__clzdi2` and `__ctzdi2` of 0 return 32 and `__clzti2` and `__ctzti2` of 0 return 64 From c2e8c2130b70a125c1b7a74700a3ffcbd945f85f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:29:59 +0000 Subject: [PATCH 218/702] docs(libSceNpUtility): drop the duplicate Net* entry and the stub it replaced from the old lists sceNpLookupCreateTitleCtxA has its own entry now, and sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment were listed twice. --- docs/dev/debt/libSceNpUtility.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceNpUtility.md b/docs/dev/debt/libSceNpUtility.md index 96d13c2cb6..b320b8f66f 100644 --- a/docs/dev/debt/libSceNpUtility.md +++ b/docs/dev/debt/libSceNpUtility.md @@ -7,9 +7,8 @@ - [sceNpAppInfoIntCheckServiceAvailabilityA, sceNpAppInfoIntCheckServiceAvailabilityAll, sceNpAppInfoIntCheckServiceAvailabilityAllA, sceNpAppInfoIntCreateRequest, sceNpAppInfoIntDestroyRequest and sceNpAppInfoIntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppInfoIntInitialize, sceNpAppLaunchLink2IntAbortRequest, sceNpAppLaunchLink2IntCreateRequest, sceNpAppLaunchLink2IntDestroyRequest, sceNpAppLaunchLink2IntFinalize and sceNpAppLaunchLink2IntGetCompatibleTitleIdList](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppLaunchLink2IntGetCompatibleTitleIdNum, sceNpAppLaunchLink2IntInitialize, sceNpAppLaunchLinkIntAbortRequest, sceNpAppLaunchLinkIntCreateRequest, sceNpAppLaunchLinkIntDestroyRequest and sceNpAppLaunchLinkIntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpLookupCreateAsyncRequest, sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppLaunchLinkIntGetCompatibleTitleIdList, sceNpAppLaunchLinkIntGetCompatibleTitleIdNum, sceNpAppLaunchLinkIntInitialize and sceNpBandwidthTestShutdownWithDetailedInfo](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpLookupCreateTitleCtxA, sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpLookupNetConvertJidToNpId, sceNpLookupNetConvertNpIdToJid, sceNpLookupNetCreateRequest, sceNpLookupNetCreateTitleCtx, sceNpLookupNetDeleteRequest, sceNpLookupNetDeleteTitleCtx and sceNpLookupNetInit](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpLookupNetInitWithFunctionPointer, sceNpLookupNetInitWithMemoryPool, sceNpLookupNetIsInit, sceNpLookupNetNpId, sceNpLookupNetSanitizeComment, sceNpLookupNetSetTimeout and sceNpLookupNetTerm](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpServiceChecker2IntAbortRequest, sceNpServiceChecker2IntCreateRequest, sceNpServiceChecker2IntDestroyRequest and sceNpServiceChecker2IntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. From 563844efe06373c5d8cb2d2a8f2be4cffd62b312 Mon Sep 17 00:00:00 2001 From: p3nicillin <lcmcclellan12@gmail.com> Date: Mon, 26 Oct 2026 13:27:35 +0000 Subject: [PATCH 219/702] feat(libkernel_sys): implement sceKernelGetCpumodeGame and sceKernelSetCpumodeGame ProsperoEden declares the setter with the mode as its argument and the getter with an int output, and measured on a PS5 that modes 0 and 1 are accepted, 2 to 7 return 0x80020016 and 1 is the default. Both threw as discovered stubs. Other modes and a null output throw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/CMakeLists.txt | 9 ++++ .../prx/libkernel_sys/Export.Discovery.cpp | 10 ----- core/libs/prx/libkernel_sys/Export.cpp | 15 +++++++ core/libs/prx/libkernel_sys/include/Api.hpp | 4 +- core/libs/tests/GuestKernelSysCpumode.cpp | 42 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 6 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 core/libs/tests/GuestKernelSysCpumode.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 354299bd3d..bfd6eb6280 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -286,6 +286,15 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_sys_console_identity PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>") endif() + add_test_executable(guest_kernel_sys_cpumode_tests tests/GuestKernelSysCpumode.cpp) + target_include_directories(guest_kernel_sys_cpumode_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_cpumode_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_cpumode_tests) + add_test(NAME guest_kernel_sys_cpumode COMMAND guest_kernel_sys_cpumode_tests) + if(WIN32) + set_tests_properties(guest_kernel_sys_cpumode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>") + endif() add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp) target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index e38c159661..e86b4ddc19 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -804,11 +804,6 @@ int APS5_VABI sceKernelGetCpuUsageThread(void) { return 0; } -int APS5_VABI sceKernelGetCpumodeGame(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelGetCurrentFanDuty(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1699,11 +1694,6 @@ int APS5_VABI sceKernelSetCoredumpAppInfo(void) { return 0; } -int APS5_VABI sceKernelSetCpumodeGame(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelSetDataTransferMode(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 6f728cbb70..e3b85114bc 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" #include <algorithm> +#include <atomic> #include <cstdarg> #include <cstdint> #include <cstddef> @@ -73,6 +74,7 @@ constexpr int GuestEexist = 17; constexpr int GuestExdev = 18; constexpr int GuestEdeadlk = 11; constexpr int GuestEagain = 35; +std::atomic<int> g_gameCpumode{1}; constexpr int GuestEloop = 62; constexpr int GuestEnametoolong = 63; constexpr int GuestEnotempty = 66; @@ -335,6 +337,19 @@ int APS5_VABI sceKernelGetOpenPsIdForSystem(void* id) { return sceKernelGetOpenPsId(id); } +int APS5_VABI sceKernelGetCpumodeGame(int* mode) { + if (mode == nullptr) throw std::runtime_error("sceKernelGetCpumodeGame: a null output faults on the console"); + *mode = g_gameCpumode.load(std::memory_order_relaxed); + return 0; +} + +int APS5_VABI sceKernelSetCpumodeGame(int mode) { + if (mode < 0 || mode > 7) NotImplemented_nid_no_patch("sceKernelSetCpumodeGame with a mode outside 0 to 7"); + if (mode > 1) return SCE_KERNEL_ERROR_EINVAL; + g_gameCpumode.store(mode, std::memory_order_relaxed); + return 0; +} + int APS5_VABI sceKernelIsCEX(void) { return 1; } diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 81089a0028..eba0580cf0 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -253,7 +253,7 @@ int APS5_VABI sceKernelGetCpuUsageAll(void); int APS5_VABI sceKernelGetCpuUsageProc(void); int APS5_VABI sceKernelGetCpuUsageProc2(void); int APS5_VABI sceKernelGetCpuUsageThread(void); -int APS5_VABI sceKernelGetCpumodeGame(void); +int APS5_VABI sceKernelGetCpumodeGame(int* mode); int APS5_VABI sceKernelGetCurrentFanDuty(void); int APS5_VABI sceKernelGetDataTransferMode(void); int APS5_VABI sceKernelGetDebugMenuMiniModeForRcmgr(void); @@ -437,7 +437,7 @@ int APS5_VABI sceKernelSetBudget(void); int APS5_VABI sceKernelSetCallRecord(void); int APS5_VABI sceKernelSetCompressionAttribute(void); int APS5_VABI sceKernelSetCoredumpAppInfo(void); -int APS5_VABI sceKernelSetCpumodeGame(void); +int APS5_VABI sceKernelSetCpumodeGame(int mode); int APS5_VABI sceKernelSetDataTransferMode(void); int APS5_VABI sceKernelSetEmergencyErrorLog(void); int APS5_VABI sceKernelSetFsstParam(void); diff --git a/core/libs/tests/GuestKernelSysCpumode.cpp b/core/libs/tests/GuestKernelSysCpumode.cpp new file mode 100644 index 0000000000..3c5cb4fd65 --- /dev/null +++ b/core/libs/tests/GuestKernelSysCpumode.cpp @@ -0,0 +1,42 @@ +#include "prx/libkernel_sys/include/Api.hpp" +#include <cstdio> +#include <cstdlib> +#include <exception> + +namespace { + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s\n", what); + std::abort(); +} + +bool Throws(void (*action)()) { + try { + action(); + } catch (const std::exception&) { + return true; + } + return false; +} + +} + +int main() { + constexpr int invalid = static_cast<int>(0x80020016u); + int mode = -1; + Require(sceKernelGetCpumodeGame(&mode) == 0 && mode == 1, "the game CPU mode does not start at 1"); + Require(sceKernelSetCpumodeGame(0) == 0, "mode 0 was refused"); + Require(sceKernelGetCpumodeGame(&mode) == 0 && mode == 0, "mode 0 was not reported back"); + for (int refused = 2; refused <= 7; ++refused) { + Require(sceKernelSetCpumodeGame(refused) == invalid, "a mode from 2 to 7 was not refused with EINVAL"); + Require(sceKernelGetCpumodeGame(&mode) == 0 && mode == 0, "a refused mode changed the game CPU mode"); + } + Require(sceKernelSetCpumodeGame(1) == 0, "mode 1 was refused"); + Require(sceKernelGetCpumodeGame(&mode) == 0 && mode == 1, "mode 1 was not reported back"); + Require(Throws([] { sceKernelSetCpumodeGame(8); }), "an unmeasured mode did not throw"); + Require(Throws([] { sceKernelSetCpumodeGame(-1); }), "a negative mode did not throw"); + Require(Throws([] { sceKernelGetCpumodeGame(nullptr); }), "a null output did not throw"); + std::puts("game CPU mode tests passed"); + return 0; +} diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index b5db329f66..51f3f599e8 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -22,6 +22,7 @@ - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. 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. Console behaviour is unverified - [link](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows resolves a symbolic link source through the host, which follows longer chains than FreeBSD: a chain of 40 links is linked, where FreeBSD 11.4 returns `ELOOP` after 32 (`MAXSYMLINKS`). A loop returns `ELOOP` as on FreeBSD - [pthread_attr_setcreatesuspend_np, pthread_resume_np, pthread_resume_all_np, pthread_multi_np, scePthreadAttrSetcreatesuspend, scePthreadResume, scePthreadResumeAll and scePthreadMulti](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - when a thread created suspended stops relative to its signal mask is unmeasured on a console. Return values and errors follow FreeBSD 11.4 libthr ([`thr_attr.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_attr.c#L369-L381), [`thr_resume_np.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_resume_np.c#L47-L97), [`thr_multi_np.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_multi_np.c#L40-L51)) and the code of firmware 13.42's `libkernel_sys.sprx` ([#9573](https://github.com/boykopovar/AnyPS5/pull/9573#issuecomment-6393877843)), where the sce forms add `0x80020000` to a non-zero errno +- [sceKernelGetCpumodeGame and sceKernelSetCpumodeGame](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the setter takes the mode as its only argument and the getter writes it through an `int*`, as [ProsperoEden (PPSA99008) declares and calls them](https://github.com/mihawk-99/PS5_ProsperoEden/blob/a0b65fe50dd48d4064e2f58fa9c8d44665478878/headless/performance.cpp#L1030-L1062); its getter faulted when called without one, which throws here. The mode starts at 1, 0 and 1 are accepted, and 2 to 7 return `EINVAL` (0x80020016), as it [measured](https://github.com/mihawk-99/PS5_ProsperoEden/blob/a0b65fe50dd48d4064e2f58fa9c8d44665478878/docs/PERFORMANCE_FINDINGS.md#L113-L114) on a PS5; other modes were not tried and throw. That the getter reports the last accepted mode is inferred from the probe. The mode changes nothing else here: on the console 0 gave system CPU mode 1 and 1 mode 5, while `sceKernelGetCpumode` still returns 0. - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). 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, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [uuidgen](../../../core/libs/prx/libkernel_sys/Uuid.cpp) (libkernel_sys) - the node is 48 random bits with the multicast bit set, chosen once per process, where the console's kernel uses the address of its first Ethernet interface; that the PS5 kernel registers one is assumed, not measured. Otherwise it follows FreeBSD 11's [`kern_uuidgen` and `sys_uuidgen`](https://github.com/freebsd/freebsd-src/blob/53c4471833e8e5e57b4d1b501b1a52b343bb0451/sys/kern/kern_uuid.c#L129-L198): version 1 UUIDs with consecutive timestamps from the guest `CLOCK_REALTIME`, a random 14-bit clock sequence that is incremented when the clock has not passed the last batch, and `EINVAL` for a count outside 1 to 2048. A null store returns `EFAULT` after the state advances, as `copyout` fails there; other invalid addresses are not detected - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. 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. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified From 7f3c05b5b891dbd6ea3ae788faa1767d33526ecd Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:48:57 -0500 Subject: [PATCH 220/702] test(libSceCdlgG2P): cover reopen and null parameter behavior in finished dialog state --- core/libs/tests/GuestG2PDialog.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index afb2800b64..d8cf406b8b 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -70,6 +70,10 @@ int main() { Require(sceG2PDialogGetStatus() == kStatusFinished); Require(sceG2PDialogUpdateStatus() == kStatusFinished); + Require(sceG2PDialogOpen(nullptr) == kErrArgNull); + Require(sceG2PDialogOpen(¶m) == 0); + Require(sceG2PDialogGetStatus() == kStatusFinished); + Require(sceG2PDialogGetResult(nullptr) == kErrArgNull); Require(sceG2PDialogGetResult(reinterpret_cast<CommonDialogResultPrefix*>(result)) == 0); Require(result[0] == -1 && result[1] == -1 && result[3] == -1); From 2de6a9f947ff6171f1e310d66e899ddb84c42325 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:50:03 -0500 Subject: [PATCH 221/702] test(libSceCamera): cover out-of-range channel rejection in settings getters and setters --- core/libs/tests/GuestCameraSettings.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestCameraSettings.cpp b/core/libs/tests/GuestCameraSettings.cpp index 8628cdbe19..45d17164ed 100644 --- a/core/libs/tests/GuestCameraSettings.cpp +++ b/core/libs/tests/GuestCameraSettings.cpp @@ -56,6 +56,8 @@ void CheckGetter(GetU32 get, std::uint32_t expected, const char* message) { Require(get(99, Channel0, &value, nullptr) == ErrorNotOpen && value == 0x5a5a5a5a, "a fabricated handle must not read settings"); Require(get(1, 0, &value, nullptr) == ErrorParam, "a getter must reject channel 0"); Require(get(1, ChannelBoth, &value, nullptr) == ErrorParam, "a getter must reject both channels"); + Require(get(1, 4, &value, nullptr) == ErrorParam, "a getter must reject channel 4"); + Require(get(1, 0xFFFFFFFFu, &value, nullptr) == ErrorParam, "a getter must reject invalid channel"); Require(get(1, Channel0, nullptr, nullptr) == ErrorParam, "a getter must reject a null result"); Require(get(1, Channel0, &value, &option) == ErrorParam, "a getter must reject an option"); Require(value == 0x5a5a5a5a, "a refused getter must not write its result"); @@ -113,7 +115,9 @@ void CheckSetter(SetU32 set, std::uint32_t largestValue) { std::uint32_t option = 0; Require(set(1, ChannelBoth, largestValue, nullptr) == ErrorNotConnected, "a setter must report NOT_CONNECTED"); Require(set(0, Channel0, 0, nullptr) == ErrorParam, "a setter must reject handle 0"); + Require(set(1, 0, 0, nullptr) == ErrorParam, "a setter must reject channel 0"); Require(set(1, 4, 0, nullptr) == ErrorParam, "a setter must reject channel 4"); + Require(set(1, 0xFFFFFFFFu, 0, nullptr) == ErrorParam, "a setter must reject invalid channel"); Require(set(1, Channel0, 0, &option) == ErrorParam, "a setter must reject an option"); if (largestValue != UINT32_MAX) Require(set(1, Channel0, largestValue + 1, nullptr) == ErrorParam, "a setter must reject a value past its range"); } From d18c3ba1692db421b2a3f6d79365ce9156785884 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:51:19 -0500 Subject: [PATCH 222/702] test(libSceCesCs): cover null source buffer in utf16 and utf32le conversions --- core/libs/tests/GuestCesUnicode.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestCesUnicode.cpp b/core/libs/tests/GuestCesUnicode.cpp index fa8fee7f88..b1282d9bf9 100644 --- a/core/libs/tests/GuestCesUnicode.cpp +++ b/core/libs/tests/GuestCesUnicode.cpp @@ -146,10 +146,14 @@ void TestErrors() { Require(sceCesUtf8ToUtf32(nullptr, 4, &consumed, &scalar) == INVALID_SRC_BUFFER); Require(sceCesUtf8ToUtf32(emoji, 4, &consumed, nullptr) == INVALID_DST_BUFFER); Require(sceCesUtf8ToUcs2(emoji, 4, &consumed, nullptr) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUtf16(nullptr, 4, &consumed, units, 2, &produced) == INVALID_SRC_BUFFER); + Require(sceCesUtf8ToUtf16be(nullptr, 4, &consumed, units, 2, &produced) == INVALID_SRC_BUFFER); + Require(sceCesUtf8ToUtf16le(nullptr, 4, &consumed, units, 2, &produced) == INVALID_SRC_BUFFER); Require(sceCesUtf8ToUtf16(emoji, 4, &consumed, nullptr, 2, &produced) == INVALID_DST_BUFFER); Require(sceCesUtf16ToUtf8(nullptr, 1, &consumed, bytes, 4, &produced) == INVALID_SRC_BUFFER); Require(sceCesUtf16ToUtf8(high, 0, &consumed, bytes, 4, &produced) == SRC_BUFFER_END); Require(sceCesUtf32beToUtf8(nullptr, bytes, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesUtf32leToUtf8(nullptr, bytes, 4, &produced) == INVALID_SRC_BUFFER); Require(sceCesUtf32ToUtf8(0, nullptr, 4, &produced) == INVALID_DST_BUFFER); Require(sceCesUtf8ToUtf32(emoji, 5, nullptr, &scalar) == 0 && scalar == 0x1F600); Require(sceCesUtf8ToUtf16(emoji, 5, nullptr, units, 2, nullptr) == 0 && units[0] == 0xD83D && units[1] == 0xDE00); From 2eda942a4226b216296feec16f8de8ccb402c371 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:52:18 -0500 Subject: [PATCH 223/702] test(libSceCesCs): cover null source and profile checks in utf16 and utf32 sbc conversions --- core/libs/tests/GuestCesWide.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestCesWide.cpp b/core/libs/tests/GuestCesWide.cpp index e644f825e5..44c46ce266 100644 --- a/core/libs/tests/GuestCesWide.cpp +++ b/core/libs/tests/GuestCesWide.cpp @@ -67,6 +67,10 @@ int main() { std::memcpy(&yLe, "\x78\x01\x00\x00", 4); Require(sceCesUtf32leToSbc(&yLe, cp1252, &sbc) == 0 && sbc == 0x9F); Require(sceCesUtf32leToSbc(nullptr, cp1252, &sbc) == invalidSrcBuffer); + Require(sceCesUtf32beToSbc(nullptr, cp1252, &sbc) == invalidSrcBuffer); + Require(sceCesUtf16beToSbc(nullptr, 1, &length, cp1252, &sbc) == invalidSrcBuffer); + Require(sceCesUtf16leToSbc(nullptr, 1, &length, cp1252, &sbc) == invalidSrcBuffer); + Require(sceCesUtf32ToSbc(0x0178, nullptr, &sbc) == invalidParameter); for (int value = 0; value < 256; ++value) { if (value == 0x98) { Require(sceCesSbcToUtf32(cp1251, static_cast<std::uint8_t>(value), &utf32) == unassignedCode); From 2f67ef6d069ad3e04ae7dec58bde7c4ed3f15e04 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:53:17 -0500 Subject: [PATCH 224/702] test(libSceCesCs): cover null source and destination checks in Japanese conversions --- core/libs/tests/GuestCesJapanese.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestCesJapanese.cpp b/core/libs/tests/GuestCesJapanese.cpp index 42b8e2c16e..5009755c54 100644 --- a/core/libs/tests/GuestCesJapanese.cpp +++ b/core/libs/tests/GuestCesJapanese.cpp @@ -328,9 +328,12 @@ int main() { std::memcpy(&utf32Be, "\x00\x00\x65\xE5", 4); Require(sceCesUtf32beToSJis(&utf32Be, sjis, mbcs, sizeof(mbcs), &produced) == 0 && std::memcmp(mbcs, "\x93\xFA", 2) == 0); Require(sceCesUtf32beToSJis(nullptr, sjis, mbcs, sizeof(mbcs), &produced) == invalidSrcBuffer); + Require(sceCesUtf32leToSJis(nullptr, sjis, mbcs, sizeof(mbcs), &produced) == invalidSrcBuffer); std::uint32_t utf32Le = 0; std::memcpy(&utf32Le, "\xE5\x65\x00\x00", 4); Require(sceCesUtf32leToEucJp(&utf32Le, euc, mbcs, sizeof(mbcs), &produced) == 0 && std::memcmp(mbcs, "\xC6\xFC", 2) == 0); + Require(sceCesUtf32leToEucJp(nullptr, euc, mbcs, sizeof(mbcs), &produced) == invalidSrcBuffer); + Require(sceCesUtf32beToEucJp(nullptr, euc, mbcs, sizeof(mbcs), &produced) == invalidSrcBuffer); constexpr int microsoftForms[] = {0x00A2, 0x00A3, 0x00A5, 0x00AC, 0x2014, 0x2016, 0x203E, 0x2212, 0x301C, 0xFEFF}; for (int code = 1; code < 0x10000; ++code) { @@ -354,6 +357,7 @@ int main() { Require(sceCesSJisCodeToJisX0208(0x0041, &ku, &ten) == invalidEncode); Require(sceCesSJisCodeToJisX0208(0x827F, &ku, &ten) == invalidEncode); Require(sceCesSJisCodeToJisX0208(0x82A0, nullptr, &ten) == invalidDstBuffer); + Require(sceCesSJisCodeToJisX0208(0x82A0, &ku, nullptr) == invalidDstBuffer); Require(sceCesJisX0208ToSJisCode(4, 2, &sjisCode) == 0 && sjisCode == 0x82A0); Require(sceCesJisX0208ToSJisCode(0, 2, &sjisCode) == invalidEncode); Require(sceCesJisX0208ToSJisCode(4, 95, &sjisCode) == invalidEncode); From d1e47bdbde518c3d4febd61fb430a8612fa1487e Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:53:59 -0500 Subject: [PATCH 225/702] test(libSceCesCs): cover null source buffer rejection in mbc conversions --- core/libs/tests/GuestCesMbc.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestCesMbc.cpp b/core/libs/tests/GuestCesMbc.cpp index 9d1e17c780..0b4dad4d08 100644 --- a/core/libs/tests/GuestCesMbc.cpp +++ b/core/libs/tests/GuestCesMbc.cpp @@ -412,10 +412,14 @@ int main() { std::uint32_t wide = 0; std::memcpy(&wide, "\x00\x00\x30\x42", 4); Require(sceCesUtf32beToMbc(&wide, sjis, mbc, sizeof(mbc), &produced) == 0 && produced == 2 && std::memcmp(mbc, "\x82\xA0", 2) == 0); + Require(sceCesUtf32beToMbc(nullptr, sjis, mbc, sizeof(mbc), &produced) == invalidSrcBuffer); + Require(sceCesUtf32leToMbc(nullptr, sjis, mbc, sizeof(mbc), &produced) == invalidSrcBuffer); Require(sceCesUtf32leToMbc(&wide, sjis, mbc, sizeof(mbc), &produced) == invalidEncode); std::uint16_t narrow = 0; std::memcpy(&narrow, "\xD8\x00", 2); Require(sceCesUtf16leToMbc(&narrow, 1, &consumed, cp1252, mbc, sizeof(mbc), &produced) == 0 && consumed == 1 && produced == 1 && mbc[0] == 0xD8); + Require(sceCesUtf16leToMbc(nullptr, 1, &consumed, cp1252, mbc, sizeof(mbc), &produced) == invalidSrcBuffer); + Require(sceCesUtf16beToMbc(nullptr, 1, &consumed, cp1252, mbc, sizeof(mbc), &produced) == invalidSrcBuffer); Require(sceCesUtf16beToMbc(&narrow, 1, &consumed, cp1252, mbc, sizeof(mbc), &produced) == srcBufferEnd); Require(sceCesUtf8ToMbc(Bytes("A\xE4\xB8\xAD"), 4, &consumed, gbk, mbc, sizeof(mbc), &produced) == 0 && consumed == 1 && produced == 1 && mbc[0] == 'A'); From cf2bfcb8e54809dacef6b8ccce464caac5225e04 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:55:02 -0500 Subject: [PATCH 226/702] test(libSceFont): cover invalid font handle in effect weight and scale setters --- core/libs/tests/GuestFontMetrics.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/tests/GuestFontMetrics.cpp b/core/libs/tests/GuestFontMetrics.cpp index 24d5f2b1ca..cd7a1c1fe6 100644 --- a/core/libs/tests/GuestFontMetrics.cpp +++ b/core/libs/tests/GuestFontMetrics.cpp @@ -128,6 +128,11 @@ int main() { Require(cleared.form == 0 && cleared.effectWeightX == 1.0f); Require(sceFontGetFontMetrics(plain, 1, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); Require(sceFontGetFontMetrics(nullptr, 1, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetEffectWeight(nullptr, 1.0f, 1.0f, 0) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetEffectWeight(plain, 1.0f, 1.0f, 1) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetEffectSlant(nullptr, 0.0f) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetScalePixel(nullptr, 16.0f, 16.0f) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontCloseFont(nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); for (FontHandle font : {plain, typo, versionOne, os2Only}) Require(sceFontCloseFont(font) == SCE_FONT_OK); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); From 0bbbb0bc8dfc329decd922a8af8c96134f36d1d9 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:55:55 -0500 Subject: [PATCH 227/702] test(libSceFont): cover parameter validation in sceFontDestroyLibrary and sceFontMemoryTerm --- core/libs/tests/GuestFontOutlineProfile.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestFontOutlineProfile.cpp b/core/libs/tests/GuestFontOutlineProfile.cpp index 1c57782d45..c91631a2c8 100644 --- a/core/libs/tests/GuestFontOutlineProfile.cpp +++ b/core/libs/tests/GuestFontOutlineProfile.cpp @@ -114,6 +114,12 @@ int main() { Require(sceFontGetFontGlyphsOutlineProfile(plain, &profile) == SCE_FONT_OK && Zero(profile)); for (FontHandle font : {profiled, plain, shortMaxp}) Require(sceFontCloseFont(font) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + FontLibrary nullLib = nullptr; + Require(sceFontDestroyLibrary(&nullLib) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(sceFontMemoryTerm(nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + FontMemory badMem{}; + Require(sceFontMemoryTerm(&badMem) == SCE_FONT_ERROR_INVALID_MEMORY); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); std::puts("Font outline profile tests passed"); From e66943d28a1d969b2cd628854c114db25c5a2a3f Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:56:47 -0500 Subject: [PATCH 228/702] test(libSceFont): cover invalid font handle in font scale slant and dpi getters --- core/libs/tests/GuestFontStyleInformation.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/libs/tests/GuestFontStyleInformation.cpp b/core/libs/tests/GuestFontStyleInformation.cpp index 21443be90b..a9fbc7470a 100644 --- a/core/libs/tests/GuestFontStyleInformation.cpp +++ b/core/libs/tests/GuestFontStyleInformation.cpp @@ -150,6 +150,17 @@ int main() { Require(sceFontGetFontStyleInformation(named, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontGetFontStyleInformation(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + float w = 0.0f; + float h = 0.0f; + float slant = 0.0f; + std::uint32_t hdpi = 0; + std::uint32_t vdpi = 0; + Require(sceFontGetScalePixel(nullptr, &w, &h) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetScalePoint(nullptr, &w, &h) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetEffectSlant(nullptr, &slant) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetResolutionDpi(nullptr, &hdpi, &vdpi) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetResolutionDpi(named, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + for (FontHandle font : {named, os2, odd}) Require(sceFontCloseFont(font) == SCE_FONT_OK); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); From c3267a8b1b6db25d4a275c19bb2ea18cb7406ff5 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:57:37 -0500 Subject: [PATCH 229/702] test(libSceAudio3d): cover terminate when uninitialized and positive reserved parameter --- core/libs/tests/GuestAudio3dParameters.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestAudio3dParameters.cpp b/core/libs/tests/GuestAudio3dParameters.cpp index 232c2e1065..41583e675d 100644 --- a/core/libs/tests/GuestAudio3dParameters.cpp +++ b/core/libs/tests/GuestAudio3dParameters.cpp @@ -98,5 +98,8 @@ int main() { CheckInvalidDestination(); CheckShortBuffer(); CheckExtendedBuffer(); + Require(sceAudio3dTerminate() == static_cast<int>(0x80EA0007), "terminate before initialize reports not ready"); + Require(sceAudio3dInitialize(1) == static_cast<int>(0x80EA0004), "positive reserved parameter rejected in sceAudio3dInitialize"); + Require(sceAudio3dInitialize(42) == static_cast<int>(0x80EA0004), "positive reserved parameter rejected in sceAudio3dInitialize"); Require(sceAudio3dInitialize(-1) == static_cast<int>(0x80EA0004), "non-zero reserved parameter rejected in sceAudio3dInitialize"); } From 3a701b0350b5b01ec4ab8e48c850c30f59e97f19 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 19:58:31 -0500 Subject: [PATCH 230/702] test(libkernel): cover null pointer and invalid type validation in mutex attributes --- core/libs/tests/GuestCondRecursiveMutex.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestCondRecursiveMutex.cpp b/core/libs/tests/GuestCondRecursiveMutex.cpp index e21300d818..e4a05e60b6 100644 --- a/core/libs/tests/GuestCondRecursiveMutex.cpp +++ b/core/libs/tests/GuestCondRecursiveMutex.cpp @@ -55,7 +55,13 @@ static int PosixTimedwait(Context& context) { static void WaitWithRecursiveMutex(int (*wait)(Context&)) { Context context; PthreadMutexattr attr = nullptr; + Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSettype(nullptr, MUTEX_TYPE_RECURSIVE) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSettype(&attr, MUTEX_TYPE_RECURSIVE) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrInit(&attr) == SCE_OK); + Require(scePthreadMutexattrSettype(&attr, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSettype(&attr, 5) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrSettype(&attr, MUTEX_TYPE_RECURSIVE) == SCE_OK); Require(scePthreadMutexInit(&context.mutex, &attr, nullptr) == SCE_OK); Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); From 7792cff8ef984f2f39d685dd1b1bc9e45a72c92a Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:33:42 +0200 Subject: [PATCH 231/702] feat(libc): implement unsigned 64-bit maximum --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/VectorAlgorithms.cpp | 14 +++++++++ core/libs/tests/GuestVectorAlgorithms.cpp | 32 +++++++++++++++++++++ docs/dev/debt/libc.md | 2 ++ 4 files changed, 49 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 441d680159..45ba94a256 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -1118,6 +1118,7 @@ const void* APS5_VABI __std_max_element_1_nid_postfix(const void* first, const v const void* APS5_VABI __std_max_element_2_nid_postfix(const void* first, const void* last, bool isSigned); const void* APS5_VABI __std_max_element_4_nid_postfix(const void* first, const void* last, bool isSigned); const void* APS5_VABI __std_max_element_8_nid_postfix(const void* first, const void* last, bool isSigned); +std::uint64_t APS5_VABI __std_max_8u_nid_postfix(const void* first, const void* last); MinMaxPointers APS5_VABI __std_minmax_element_1_nid_postfix(const void* first, const void* last, bool isSigned); MinMaxPointers APS5_VABI __std_minmax_element_2_nid_postfix(const void* first, const void* last, bool isSigned); MinMaxPointers APS5_VABI __std_minmax_element_4_nid_postfix(const void* first, const void* last, bool isSigned); diff --git a/core/libs/prx/libc/src/VectorAlgorithms.cpp b/core/libs/prx/libc/src/VectorAlgorithms.cpp index 03b7a62dd3..56f701c18e 100644 --- a/core/libs/prx/libc/src/VectorAlgorithms.cpp +++ b/core/libs/prx/libc/src/VectorAlgorithms.cpp @@ -143,6 +143,20 @@ const void* APS5_VABI __std_max_element_1_nid_postfix(const void* first, const v const void* APS5_VABI __std_max_element_2_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint16_t>(first, last, isSigned, false, true).maximum; } const void* APS5_VABI __std_max_element_4_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint32_t>(first, last, isSigned, false, true).maximum; } const void* APS5_VABI __std_max_element_8_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint64_t>(first, last, isSigned, false, true).maximum; } +std::uint64_t APS5_VABI __std_max_8u_nid_postfix(const void* first, const void* last) { + const auto beginAddress = reinterpret_cast<std::uintptr_t>(first); + const auto endAddress = reinterpret_cast<std::uintptr_t>(last); + if (!first || !last || endAddress <= beginAddress || (endAddress - beginAddress) % sizeof(std::uint64_t) != 0) NotImplemented_nid_no_patch(__func__); + auto* current = static_cast<const unsigned char*>(first); + const auto* end = static_cast<const unsigned char*>(last); + std::uint64_t maximum = Read<std::uint64_t>(current); + for (current += sizeof(std::uint64_t); current != end; current += sizeof(std::uint64_t)) { + const auto value = Read<std::uint64_t>(current); + if (maximum < value) maximum = value; + } + return maximum; +} + MinMaxPointers APS5_VABI __std_minmax_element_1_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint8_t>(first, last, isSigned, true, true); } MinMaxPointers APS5_VABI __std_minmax_element_2_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint16_t>(first, last, isSigned, true, true); } MinMaxPointers APS5_VABI __std_minmax_element_4_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint32_t>(first, last, isSigned, true, true); } diff --git a/core/libs/tests/GuestVectorAlgorithms.cpp b/core/libs/tests/GuestVectorAlgorithms.cpp index d9c21878c3..9cb382037c 100644 --- a/core/libs/tests/GuestVectorAlgorithms.cpp +++ b/core/libs/tests/GuestVectorAlgorithms.cpp @@ -177,12 +177,44 @@ static void CheckReverseAndSwap() { for (int i = 0; i < 37; ++i) Require(left[i] == 100 + i && right[i] == i); } +static void CheckMaxValue() { + const std::uint64_t edges[] = {0, 1, 0xffffffff, 0x100000000, 0x7fffffffffffffff, 0x8000000000000000, 0xfffffffffffffffe, 0xffffffffffffffff}; + for (const auto& left : edges) { + Require(__std_max_8u_nid_postfix(&left, &left + 1) == left); + for (const auto& right : edges) { + const std::uint64_t pair[] = {left, right}; + Require(__std_max_8u_nid_postfix(pair, pair + 2) == std::max(left, right)); + } + } + std::uint64_t state = 0x7698f1b33a25c04d; + for (const std::size_t count : {1, 2, 3, 4, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 257}) { + std::vector<std::uint64_t> values(count); + for (auto& value : values) { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + value = state; + } + const auto expected = *std::max_element(values.begin(), values.end()); + for (std::size_t offset = 0; offset < 16; ++offset) { + std::vector<unsigned char> storage(count * sizeof(std::uint64_t) + 32, 0xff); + std::memcpy(storage.data() + offset, values.data(), count * sizeof(std::uint64_t)); + const auto original = storage; + Require(__std_max_8u_nid_postfix(storage.data() + offset, storage.data() + offset + count * sizeof(std::uint64_t)) == expected); + Require(storage == original); + } + } + const std::uint64_t repeated[] = {9, 9, 9, 9}; + Require(__std_max_8u_nid_postfix(repeated, repeated + 4) == 9); +} + int main() { CheckMinValue(); CheckSizedHelpers(); CheckFindAndCount(); CheckUnsizedRescan(); CheckMinMax(); + CheckMaxValue(); CheckReverseAndSwap(); return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..fbe3918036 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -18,6 +18,8 @@ - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) rounds with the MXCSR rounding mode, as the console's `libSceLibcInternal.sprx` does in a payload process on a PS5 (firmware 13.60): with the x87 mode set to upward and MXCSR to nearest it rounded 2.5 to 2, and with MXCSR upward to 3, and the four modes gave the results the host gives. The console also raised `FE_INEXACT` for those inexact inputs, which the host's `nearbyintf` does not; whether it does for exact inputs was not measured - [__std_min_8u](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `i8DeU1sI6I4`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, unsigned 64-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x62a30`, size `0x2a4`, read from disassembly and run directly on the host, not measured on a console. - [__std_find_trivial_unsized_N](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc) - matches the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs): when nothing matches between `first` and the end of its 32-byte-aligned block, the console rescans that whole block without masking and so can return a match up to 31 bytes before `first` +- [__std_max_8u](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `Lg0dYZuDrz4`) - null, empty, reversed and partial-element ranges throw; native failure behavior for those inputs is not reproduced. The nonempty-range ABI and unsigned 64-bit result follow the `libc.prx` bundled with PPSA03671, read from disassembly and compared on a Windows host without running its initializers, not measured on a console. + - [_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` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. 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. - [setjmp, longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) save the signal mask at 72 in the jump buffer and restore it, as FreeBSD 11.4's [`setjmp` and `longjmp`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/amd64/gen/setjmp.S#L51-L85) do, which is unverified on a console. They go through the `sigprocmask` libkernel registers when it loads and mark the buffer at 88 as `sigsetjmp` does, so a buffer saved before then is jumped to without changing the mask - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. From be750a1f384f784cfb5c99e5682359ac1c5bc778 Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:21:41 +0100 Subject: [PATCH 232/702] feat(libc): implement unsigned 32-bit minimum --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/VectorAlgorithms.cpp | 14 ++++++++ core/libs/tests/GuestVectorAlgorithms.cpp | 39 +++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 55 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 441d680159..b32dc8d50b 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -1122,6 +1122,7 @@ MinMaxPointers APS5_VABI __std_minmax_element_1_nid_postfix(const void* first, c MinMaxPointers APS5_VABI __std_minmax_element_2_nid_postfix(const void* first, const void* last, bool isSigned); MinMaxPointers APS5_VABI __std_minmax_element_4_nid_postfix(const void* first, const void* last, bool isSigned); MinMaxPointers APS5_VABI __std_minmax_element_8_nid_postfix(const void* first, const void* last, bool isSigned); +std::uint32_t APS5_VABI __std_min_4u_nid_postfix(const void* first, const void* last); void APS5_VABI __std_reverse_trivially_swappable_1_nid_postfix(void* first, void* last); void APS5_VABI __std_reverse_trivially_swappable_2_nid_postfix(void* first, void* last); void APS5_VABI __std_reverse_trivially_swappable_4_nid_postfix(void* first, void* last); diff --git a/core/libs/prx/libc/src/VectorAlgorithms.cpp b/core/libs/prx/libc/src/VectorAlgorithms.cpp index 03b7a62dd3..f7e58d61f8 100644 --- a/core/libs/prx/libc/src/VectorAlgorithms.cpp +++ b/core/libs/prx/libc/src/VectorAlgorithms.cpp @@ -148,6 +148,20 @@ MinMaxPointers APS5_VABI __std_minmax_element_2_nid_postfix(const void* first, c MinMaxPointers APS5_VABI __std_minmax_element_4_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint32_t>(first, last, isSigned, true, true); } MinMaxPointers APS5_VABI __std_minmax_element_8_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint64_t>(first, last, isSigned, true, true); } +std::uint32_t APS5_VABI __std_min_4u_nid_postfix(const void* first, const void* last) { + const auto beginAddress = reinterpret_cast<std::uintptr_t>(first); + const auto endAddress = reinterpret_cast<std::uintptr_t>(last); + if (!first || !last || endAddress <= beginAddress || (endAddress - beginAddress) % sizeof(std::uint32_t) != 0) NotImplemented_nid_no_patch(__func__); + auto* current = static_cast<const unsigned char*>(first); + const auto* end = static_cast<const unsigned char*>(last); + std::uint32_t minimum = Read<std::uint32_t>(current); + for (current += sizeof(std::uint32_t); current != end; current += sizeof(std::uint32_t)) { + const auto value = Read<std::uint32_t>(current); + if (value < minimum) minimum = value; + } + return minimum; +} + void APS5_VABI __std_reverse_trivially_swappable_1_nid_postfix(void* first, void* last) { Reverse<1>(first, last); } void APS5_VABI __std_reverse_trivially_swappable_2_nid_postfix(void* first, void* last) { Reverse<2>(first, last); } void APS5_VABI __std_reverse_trivially_swappable_4_nid_postfix(void* first, void* last) { Reverse<4>(first, last); } diff --git a/core/libs/tests/GuestVectorAlgorithms.cpp b/core/libs/tests/GuestVectorAlgorithms.cpp index d9c21878c3..e11000ed17 100644 --- a/core/libs/tests/GuestVectorAlgorithms.cpp +++ b/core/libs/tests/GuestVectorAlgorithms.cpp @@ -157,6 +157,44 @@ static void CheckMinMax() { Require(empty.minimum == wides && empty.maximum == wides); } +static void CheckUnsignedMin32Value() { + const std::uint32_t edges[] = {0, 1, 0xffff, 0x10000, 0x7fffffff, 0x80000000, 0xfffffffe, 0xffffffff}; + for (const auto& left : edges) { + Require(__std_min_4u_nid_postfix(&left, &left + 1) == left); + for (const auto& right : edges) { + const std::uint32_t pair[] = {left, right}; + Require(__std_min_4u_nid_postfix(pair, pair + 2) == std::min(left, right)); + } + } + std::uint32_t state = 0x8e47396b; + for (const std::size_t count : {1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 257}) { + std::vector<std::uint32_t> values(count); + for (auto& value : values) { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + value = state; + } + const auto expected = *std::min_element(values.begin(), values.end()); + for (std::size_t offset = 0; offset < 32; ++offset) { + std::vector<unsigned char> storage(count * sizeof(std::uint32_t) + 64, 0xff); + std::memcpy(storage.data() + offset, values.data(), count * sizeof(std::uint32_t)); + const auto original = storage; + Require(__std_min_4u_nid_postfix(storage.data() + offset, storage.data() + offset + count * sizeof(std::uint32_t)) == expected); + Require(storage == original); + } + std::fill(values.begin(), values.end(), UINT32_MAX); + Require(__std_min_4u_nid_postfix(values.data(), values.data() + count) == UINT32_MAX); + for (std::size_t position = 0; position < count; ++position) { + values[position] = 0x10005; + Require(__std_min_4u_nid_postfix(values.data(), values.data() + count) == 0x10005); + values[position] = UINT32_MAX; + } + } + const std::uint32_t repeated[] = {9, 9, 9, 9}; + Require(__std_min_4u_nid_postfix(repeated, repeated + 4) == 9); +} + static void CheckReverseAndSwap() { std::uint16_t halves[] = {1, 2, 3, 4, 5}; __std_reverse_trivially_swappable_2_nid_postfix(halves, halves + 5); @@ -181,6 +219,7 @@ int main() { CheckMinValue(); CheckSizedHelpers(); CheckFindAndCount(); + CheckUnsignedMin32Value(); CheckUnsizedRescan(); CheckMinMax(); CheckReverseAndSwap(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..8d2745e1f0 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -16,6 +16,7 @@ - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller. The `libc.prx` that titles ship (read from disassembly of PPSA13934's copy, 0x5cf0, not measured on a console) passes its argument to `scePthreadMutexTrylock` and returns 0 for success, `_Thrd_busy` (3) for `SCE_KERNEL_ERROR_EBUSY` and `_Thrd_error` (4) for any other error, so busy for an owned non-recursive mutex comes from the kernel mutex there; this libc keeps its own owner and count instead, and its result for a kernel error other than busy is not compared. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) rounds with the MXCSR rounding mode, as the console's `libSceLibcInternal.sprx` does in a payload process on a PS5 (firmware 13.60): with the x87 mode set to upward and MXCSR to nearest it rounded 2.5 to 2, and with MXCSR upward to 3, and the four modes gave the results the host gives. The console also raised `FE_INEXACT` for those inexact inputs, which the host's `nearbyintf` does not; whether it does for exact inputs was not measured +- [__std_min_4u](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `60rcqDrkUNY`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, unsigned 32-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x62680`, size `0x1a2`, read from disassembly and run directly on the host, not measured on a console. - [__std_min_8u](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `i8DeU1sI6I4`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, unsigned 64-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x62a30`, size `0x2a4`, read from disassembly and run directly on the host, not measured on a console. - [__std_find_trivial_unsized_N](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc) - matches the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs): when nothing matches between `first` and the end of its 32-byte-aligned block, the console rescans that whole block without masking and so can return a match up to 31 bytes before `first` - [_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` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. 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. From 80bc31bb6c6828bfa6b4e9e90f29575f3d55411c Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:51:18 +0100 Subject: [PATCH 233/702] feat(libc): implement signed 64-bit minimum --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/VectorAlgorithms.cpp | 14 ++++++++ core/libs/tests/GuestVectorAlgorithms.cpp | 39 +++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 55 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 441d680159..b4bcea0a0e 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -1131,6 +1131,7 @@ void APS5_VABI __std_reverse_copy_trivially_copyable_2_nid_postfix(const void* f void APS5_VABI __std_reverse_copy_trivially_copyable_4_nid_postfix(const void* first, const void* last, void* destination); void APS5_VABI __std_reverse_copy_trivially_copyable_8_nid_postfix(const void* first, const void* last, void* destination); void APS5_VABI __std_swap_ranges_trivially_swappable_noalias_nid_postfix(void* first1, void* last1, void* first2); +std::int64_t APS5_VABI __std_min_8i_nid_postfix(const void* first, const void* last); void APS5_VABI ignore_handler_s_nid_postfix(const char* message, void* pointer, int error); void* APS5_VABI set_constraint_handler_s_nid_postfix(void* handler); std::size_t APS5_VABI wcstombs_nid_postfix(char* destination, const std::uint16_t* source, std::size_t capacity); diff --git a/core/libs/prx/libc/src/VectorAlgorithms.cpp b/core/libs/prx/libc/src/VectorAlgorithms.cpp index 03b7a62dd3..4e176f010f 100644 --- a/core/libs/prx/libc/src/VectorAlgorithms.cpp +++ b/core/libs/prx/libc/src/VectorAlgorithms.cpp @@ -168,4 +168,18 @@ void APS5_VABI __std_swap_ranges_trivially_swappable_noalias_nid_postfix(void* f } } +std::int64_t APS5_VABI __std_min_8i_nid_postfix(const void* first, const void* last) { + const auto beginAddress = reinterpret_cast<std::uintptr_t>(first); + const auto endAddress = reinterpret_cast<std::uintptr_t>(last); + if (!first || !last || endAddress <= beginAddress || (endAddress - beginAddress) % sizeof(std::int64_t) != 0) NotImplemented_nid_no_patch(__func__); + auto* current = static_cast<const unsigned char*>(first); + const auto* end = static_cast<const unsigned char*>(last); + std::int64_t minimum = Read<std::int64_t>(current); + for (current += sizeof(std::int64_t); current != end; current += sizeof(std::int64_t)) { + const auto value = Read<std::int64_t>(current); + if (value < minimum) minimum = value; + } + return minimum; +} + } diff --git a/core/libs/tests/GuestVectorAlgorithms.cpp b/core/libs/tests/GuestVectorAlgorithms.cpp index d9c21878c3..499bb8a58d 100644 --- a/core/libs/tests/GuestVectorAlgorithms.cpp +++ b/core/libs/tests/GuestVectorAlgorithms.cpp @@ -82,6 +82,44 @@ static void CheckSizedHelpers() { Require(__std_max_element_8_nid_postfix(wides, wides, false) == wides); } +static void CheckSignedMin64Value() { + const std::int64_t edges[] = {INT64_MIN, INT64_MIN + 1, -0x100000001ll, -0x100000000ll, -1, 0, 1, 0x7fffffff, 0xffffffff, 0x100000000, INT64_MAX - 1, INT64_MAX}; + for (const auto& left : edges) { + Require(__std_min_8i_nid_postfix(&left, &left + 1) == left); + for (const auto& right : edges) { + const std::int64_t pair[] = {left, right}; + Require(__std_min_8i_nid_postfix(pair, pair + 2) == std::min(left, right)); + } + } + std::uint64_t state = 0xf3c82d51479ab6e1; + for (const std::size_t count : {1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 257}) { + std::vector<std::int64_t> values(count); + for (auto& value : values) { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + std::memcpy(&value, &state, sizeof(value)); + } + const auto expected = *std::min_element(values.begin(), values.end()); + for (std::size_t offset = 0; offset < 32; ++offset) { + std::vector<unsigned char> storage(count * sizeof(std::int64_t) + 64, 0xff); + std::memcpy(storage.data() + offset, values.data(), count * sizeof(std::int64_t)); + const auto original = storage; + Require(__std_min_8i_nid_postfix(storage.data() + offset, storage.data() + offset + count * sizeof(std::int64_t)) == expected); + Require(storage == original); + } + std::fill(values.begin(), values.end(), INT64_MAX); + Require(__std_min_8i_nid_postfix(values.data(), values.data() + count) == INT64_MAX); + for (std::size_t position = 0; position < count; ++position) { + values[position] = -0x100000005ll; + Require(__std_min_8i_nid_postfix(values.data(), values.data() + count) == -0x100000005ll); + values[position] = INT64_MAX; + } + } + const std::int64_t repeated[] = {-9, -9, -9, -9}; + Require(__std_min_8i_nid_postfix(repeated, repeated + 4) == -9); +} + static void CheckFindAndCount() { const std::uint16_t halves[] = {5, 7, 9, 7}; Require(__std_find_trivial_2_nid_postfix(halves, halves + 4, 7) == halves + 1); @@ -184,5 +222,6 @@ int main() { CheckUnsizedRescan(); CheckMinMax(); CheckReverseAndSwap(); + CheckSignedMin64Value(); return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..29709b6fdd 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -21,6 +21,7 @@ - [_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` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. 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. - [setjmp, longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) save the signal mask at 72 in the jump buffer and restore it, as FreeBSD 11.4's [`setjmp` and `longjmp`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/amd64/gen/setjmp.S#L51-L85) do, which is unverified on a console. They go through the `sigprocmask` libkernel registers when it loads and mark the buffer at 88 as `sigsetjmp` does, so a buffer saved before then is jumped to without changing the mask - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. +- [__std_min_8i](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `HWeZH0iNZU8`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, signed 64-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x62830`, size `0x1f4`, read from disassembly and run directly on the host, not measured on a console. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow a payload process on a PS5 (firmware 13.60): the rounding modes are x87 control bits 10-11 (0, 0x400, 0x800, 0xc00), `fesetround` sets them and the MXCSR rounding bits and returns -1 for any other value, `fegetround` reads the x87 bits, and the exception flags are 1, 4, 8, 0x10 and 0x20 with a 2-byte `fexcept_t`. `fetestexcept` returns the x87 status word and MXCSR together, masked only by its argument, so a mask past 0x3f also returns their other bits. They access the current host thread's x87 and MXCSR state, which starts with flush-to-zero and denormals-are-zero set on the console. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. Null flag pointers throw instead of faulting. - [feenableexcept, fedisableexcept, __feenableexcept, __fedisableexcept, fegettrapenable and fesettrapenable](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libc) change only the x87 masks under Rosetta 2: MXCSR keeps its six exception masks set there, so no SSE trap is enabled, and the enable and disable results, which report the traps enabled on both units, report none From 4b911d7bae592ac57251b0b1b393b68cea830325 Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:12:46 +0100 Subject: [PATCH 234/702] feat(libc): implement signed 64-bit maximum --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/VectorAlgorithms.cpp | 15 ++++++++ core/libs/tests/GuestVectorAlgorithms.cpp | 39 +++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 56 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 441d680159..e6811bc0d6 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -1105,6 +1105,7 @@ const void* APS5_VABI __std_find_trivial_unsized_1_nid_postfix(const void* first const void* APS5_VABI __std_find_trivial_unsized_2_nid_postfix(const void* first, std::uint16_t value); const void* APS5_VABI __std_find_trivial_unsized_4_nid_postfix(const void* first, std::uint32_t value); const void* APS5_VABI __std_find_trivial_unsized_8_nid_postfix(const void* first, std::uint64_t value); +std::int64_t APS5_VABI __std_max_8i_nid_postfix(const void* first, const void* last); std::size_t APS5_VABI __std_count_trivial_1_nid_postfix(const void* first, const void* last, std::uint8_t value); std::size_t APS5_VABI __std_count_trivial_2_nid_postfix(const void* first, const void* last, std::uint16_t value); std::size_t APS5_VABI __std_count_trivial_4_nid_postfix(const void* first, const void* last, std::uint32_t value); diff --git a/core/libs/prx/libc/src/VectorAlgorithms.cpp b/core/libs/prx/libc/src/VectorAlgorithms.cpp index 03b7a62dd3..b54c66e5a6 100644 --- a/core/libs/prx/libc/src/VectorAlgorithms.cpp +++ b/core/libs/prx/libc/src/VectorAlgorithms.cpp @@ -116,6 +116,21 @@ const void* APS5_VABI __std_find_trivial_unsized_1_nid_postfix(const void* first const void* APS5_VABI __std_find_trivial_unsized_2_nid_postfix(const void* first, std::uint16_t value) { return FindUnsized(first, value); } const void* APS5_VABI __std_find_trivial_unsized_4_nid_postfix(const void* first, std::uint32_t value) { return FindUnsized(first, value); } const void* APS5_VABI __std_find_trivial_unsized_8_nid_postfix(const void* first, std::uint64_t value) { return FindUnsized(first, value); } + +std::int64_t APS5_VABI __std_max_8i_nid_postfix(const void* first, const void* last) { + const auto beginAddress = reinterpret_cast<std::uintptr_t>(first); + const auto endAddress = reinterpret_cast<std::uintptr_t>(last); + if (!first || !last || endAddress <= beginAddress || (endAddress - beginAddress) % sizeof(std::int64_t) != 0) NotImplemented_nid_no_patch(__func__); + auto* current = static_cast<const unsigned char*>(first); + const auto* end = static_cast<const unsigned char*>(last); + std::int64_t maximum = Read<std::int64_t>(current); + for (current += sizeof(std::int64_t); current != end; current += sizeof(std::int64_t)) { + const auto value = Read<std::int64_t>(current); + if (value > maximum) maximum = value; + } + return maximum; +} + std::size_t APS5_VABI __std_count_trivial_1_nid_postfix(const void* first, const void* last, std::uint8_t value) { return Count(first, last, value); } std::size_t APS5_VABI __std_count_trivial_2_nid_postfix(const void* first, const void* last, std::uint16_t value) { return Count(first, last, value); } std::size_t APS5_VABI __std_count_trivial_4_nid_postfix(const void* first, const void* last, std::uint32_t value) { return Count(first, last, value); } diff --git a/core/libs/tests/GuestVectorAlgorithms.cpp b/core/libs/tests/GuestVectorAlgorithms.cpp index d9c21878c3..debec4615f 100644 --- a/core/libs/tests/GuestVectorAlgorithms.cpp +++ b/core/libs/tests/GuestVectorAlgorithms.cpp @@ -95,6 +95,44 @@ static void CheckFindAndCount() { Require(__std_find_trivial_unsized_4_nid_postfix(words + 8, 42) == words + 12); } +static void CheckSignedMax64Value() { + const std::int64_t edges[] = {INT64_MIN, INT64_MIN + 1, -0x100000001ll, -0x100000000ll, -1, 0, 1, 0x7fffffff, 0xffffffff, 0x100000000, INT64_MAX - 1, INT64_MAX}; + for (const auto& left : edges) { + Require(__std_max_8i_nid_postfix(&left, &left + 1) == left); + for (const auto& right : edges) { + const std::int64_t pair[] = {left, right}; + Require(__std_max_8i_nid_postfix(pair, pair + 2) == std::max(left, right)); + } + } + std::uint64_t state = 0x2e791fa84d6c530b; + for (const std::size_t count : {1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 257}) { + std::vector<std::int64_t> values(count); + for (auto& value : values) { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + std::memcpy(&value, &state, sizeof(value)); + } + const auto expected = *std::max_element(values.begin(), values.end()); + for (std::size_t offset = 0; offset < 32; ++offset) { + std::vector<unsigned char> storage(count * sizeof(std::int64_t) + 64, 0xff); + std::memcpy(storage.data() + offset, values.data(), count * sizeof(std::int64_t)); + const auto original = storage; + Require(__std_max_8i_nid_postfix(storage.data() + offset, storage.data() + offset + count * sizeof(std::int64_t)) == expected); + Require(storage == original); + } + std::fill(values.begin(), values.end(), INT64_MIN); + Require(__std_max_8i_nid_postfix(values.data(), values.data() + count) == INT64_MIN); + for (std::size_t position = 0; position < count; ++position) { + values[position] = -0x100000005ll; + Require(__std_max_8i_nid_postfix(values.data(), values.data() + count) == -0x100000005ll); + values[position] = INT64_MIN; + } + } + const std::int64_t repeated[] = {-9, -9, -9, -9}; + Require(__std_max_8i_nid_postfix(repeated, repeated + 4) == -9); +} + static void CheckUnsizedRescan() { alignas(32) unsigned char bytes[96] = {}; bytes[3] = 'x'; @@ -182,6 +220,7 @@ int main() { CheckSizedHelpers(); CheckFindAndCount(); CheckUnsizedRescan(); + CheckSignedMax64Value(); CheckMinMax(); CheckReverseAndSwap(); return 0; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..42a55aaf3c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -13,6 +13,7 @@ - [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when the title's replacement table has an allocator entry and the exports belong to one native module. With an empty table it keeps the default heap, since a bundled `libc.prx` may export the allocator NIDs while forwarding them to HLE. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - a closed or unknown descriptor returns null with `EBADF`, where the system `libSceLibcInternal` of PS5 firmware 13.60 returns a stream whose later flush and close fail with `EBADF`; a null mode returns null with `EINVAL`, where the console returns a stream; on Linux a mode the descriptor's access does not allow returns null with `EINVAL`, as the host `fdopen` checks it. Otherwise it follows the console: a mode whose first letter is not `r`, `w` or `a` fails with `EINVAL` before the descriptor is looked at, a negative descriptor then returns null without setting `errno`, and the descriptor's access mode is not checked, so the first read or write it does not allow fails instead - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. +- [__std_max_8i](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `oLoo17ObE4A`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, signed 64-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x63790`, size `0x1f4`, read from disassembly and run directly on the host, not measured on a console. - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller. The `libc.prx` that titles ship (read from disassembly of PPSA13934's copy, 0x5cf0, not measured on a console) passes its argument to `scePthreadMutexTrylock` and returns 0 for success, `_Thrd_busy` (3) for `SCE_KERNEL_ERROR_EBUSY` and `_Thrd_error` (4) for any other error, so busy for an owned non-recursive mutex comes from the kernel mutex there; this libc keeps its own owner and count instead, and its result for a kernel error other than busy is not compared. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) rounds with the MXCSR rounding mode, as the console's `libSceLibcInternal.sprx` does in a payload process on a PS5 (firmware 13.60): with the x87 mode set to upward and MXCSR to nearest it rounded 2.5 to 2, and with MXCSR upward to 3, and the four modes gave the results the host gives. The console also raised `FE_INEXACT` for those inexact inputs, which the host's `nearbyintf` does not; whether it does for exact inputs was not measured From 5f08cbf1b3d800915add2582a2c8c7ccd3551f36 Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:48:20 +0100 Subject: [PATCH 235/702] feat(libc): implement unsigned 32-bit maximum --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/VectorAlgorithms.cpp | 14 ++++++++ core/libs/tests/GuestVectorAlgorithms.cpp | 39 +++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 55 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 441d680159..f5f1575c4e 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -1126,6 +1126,7 @@ void APS5_VABI __std_reverse_trivially_swappable_1_nid_postfix(void* first, void void APS5_VABI __std_reverse_trivially_swappable_2_nid_postfix(void* first, void* last); void APS5_VABI __std_reverse_trivially_swappable_4_nid_postfix(void* first, void* last); void APS5_VABI __std_reverse_trivially_swappable_8_nid_postfix(void* first, void* last); +std::uint32_t APS5_VABI __std_max_4u_nid_postfix(const void* first, const void* last); void APS5_VABI __std_reverse_copy_trivially_copyable_1_nid_postfix(const void* first, const void* last, void* destination); void APS5_VABI __std_reverse_copy_trivially_copyable_2_nid_postfix(const void* first, const void* last, void* destination); void APS5_VABI __std_reverse_copy_trivially_copyable_4_nid_postfix(const void* first, const void* last, void* destination); diff --git a/core/libs/prx/libc/src/VectorAlgorithms.cpp b/core/libs/prx/libc/src/VectorAlgorithms.cpp index 03b7a62dd3..43f008ee42 100644 --- a/core/libs/prx/libc/src/VectorAlgorithms.cpp +++ b/core/libs/prx/libc/src/VectorAlgorithms.cpp @@ -152,6 +152,20 @@ void APS5_VABI __std_reverse_trivially_swappable_1_nid_postfix(void* first, void void APS5_VABI __std_reverse_trivially_swappable_2_nid_postfix(void* first, void* last) { Reverse<2>(first, last); } void APS5_VABI __std_reverse_trivially_swappable_4_nid_postfix(void* first, void* last) { Reverse<4>(first, last); } void APS5_VABI __std_reverse_trivially_swappable_8_nid_postfix(void* first, void* last) { Reverse<8>(first, last); } +std::uint32_t APS5_VABI __std_max_4u_nid_postfix(const void* first, const void* last) { + const auto beginAddress = reinterpret_cast<std::uintptr_t>(first); + const auto endAddress = reinterpret_cast<std::uintptr_t>(last); + if (!first || !last || endAddress <= beginAddress || (endAddress - beginAddress) % sizeof(std::uint32_t) != 0) NotImplemented_nid_no_patch(__func__); + auto* current = static_cast<const unsigned char*>(first); + const auto* end = static_cast<const unsigned char*>(last); + std::uint32_t maximum = Read<std::uint32_t>(current); + for (current += sizeof(std::uint32_t); current != end; current += sizeof(std::uint32_t)) { + const auto value = Read<std::uint32_t>(current); + if (value > maximum) maximum = value; + } + return maximum; +} + void APS5_VABI __std_reverse_copy_trivially_copyable_1_nid_postfix(const void* first, const void* last, void* destination) { ReverseCopy<1>(first, last, destination); } void APS5_VABI __std_reverse_copy_trivially_copyable_2_nid_postfix(const void* first, const void* last, void* destination) { ReverseCopy<2>(first, last, destination); } void APS5_VABI __std_reverse_copy_trivially_copyable_4_nid_postfix(const void* first, const void* last, void* destination) { ReverseCopy<4>(first, last, destination); } diff --git a/core/libs/tests/GuestVectorAlgorithms.cpp b/core/libs/tests/GuestVectorAlgorithms.cpp index d9c21878c3..3a9a5a0b7e 100644 --- a/core/libs/tests/GuestVectorAlgorithms.cpp +++ b/core/libs/tests/GuestVectorAlgorithms.cpp @@ -137,6 +137,44 @@ static void CheckUnsizedRescan() { Require(__std_find_trivial_unsized_8_nid_postfix(wides + 1, 99) == wides + 6); } +static void CheckUnsignedMax32Value() { + const std::uint32_t edges[] = {0, 1, 0xffff, 0x10000, 0x7fffffff, 0x80000000, 0xfffffffe, 0xffffffff}; + for (const auto& left : edges) { + Require(__std_max_4u_nid_postfix(&left, &left + 1) == left); + for (const auto& right : edges) { + const std::uint32_t pair[] = {left, right}; + Require(__std_max_4u_nid_postfix(pair, pair + 2) == std::max(left, right)); + } + } + std::uint32_t state = 0x8e47396b; + for (const std::size_t count : {1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 257}) { + std::vector<std::uint32_t> values(count); + for (auto& value : values) { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + value = state; + } + const auto expected = *std::max_element(values.begin(), values.end()); + for (std::size_t offset = 0; offset < 32; ++offset) { + std::vector<unsigned char> storage(count * sizeof(std::uint32_t) + 64, 0xff); + std::memcpy(storage.data() + offset, values.data(), count * sizeof(std::uint32_t)); + const auto original = storage; + Require(__std_max_4u_nid_postfix(storage.data() + offset, storage.data() + offset + count * sizeof(std::uint32_t)) == expected); + Require(storage == original); + } + std::fill(values.begin(), values.end(), 0); + Require(__std_max_4u_nid_postfix(values.data(), values.data() + count) == 0); + for (std::size_t position = 0; position < count; ++position) { + values[position] = 0x10005; + Require(__std_max_4u_nid_postfix(values.data(), values.data() + count) == 0x10005); + values[position] = 0; + } + } + const std::uint32_t repeated[] = {9, 9, 9, 9}; + Require(__std_max_4u_nid_postfix(repeated, repeated + 4) == 9); +} + static void CheckMinMax() { const unsigned char bytes[] = {3, 0x80, 1, 0x80, 1, 0xff}; Require(__std_min_element_1_nid_postfix(bytes, bytes + 6, false) == bytes + 2); @@ -180,6 +218,7 @@ static void CheckReverseAndSwap() { int main() { CheckMinValue(); CheckSizedHelpers(); + CheckUnsignedMax32Value(); CheckFindAndCount(); CheckUnsizedRescan(); CheckMinMax(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..b0f95bf67d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -24,6 +24,7 @@ - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow a payload process on a PS5 (firmware 13.60): the rounding modes are x87 control bits 10-11 (0, 0x400, 0x800, 0xc00), `fesetround` sets them and the MXCSR rounding bits and returns -1 for any other value, `fegetround` reads the x87 bits, and the exception flags are 1, 4, 8, 0x10 and 0x20 with a 2-byte `fexcept_t`. `fetestexcept` returns the x87 status word and MXCSR together, masked only by its argument, so a mask past 0x3f also returns their other bits. They access the current host thread's x87 and MXCSR state, which starts with flush-to-zero and denormals-are-zero set on the console. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. Null flag pointers throw instead of faulting. - [feenableexcept, fedisableexcept, __feenableexcept, __fedisableexcept, fegettrapenable and fesettrapenable](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libc) change only the x87 masks under Rosetta 2: MXCSR keeps its six exception masks set there, so no SSE trap is enabled, and the enable and disable results, which report the traps enabled on both units, report none +- [__std_max_4u](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `fxmF53LHALE`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, unsigned 32-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x635e0`, size `0x1a2`, read from disassembly and run directly on the host, not measured on a console. - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting - [feholdexcept](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `1kduKXMqx7k`) - console equivalence and module/library versions are unverified. The signature and 32-byte amd64 environment layout follow the public SDK [fenv.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/fenv.h); saving both units, masking exceptions and clearing pending flags follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c). The NID comes from the public 3.20 stub inventory, not a module examined here. Null storage throws before modifying the environment, where the reference dereferences it - [feupdateenv, __fe_dfl_env](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `YXQ4gXamCrY`) - a null environment or MXCSR bits the host's `FXSAVE` mask does not support (zero mask: `0xffbf`) throw before changing state, where the system `libSceLibcInternal` of PS5 firmware 13.60 faults (`SIGSEGV`, and `SIGBUS` for MXCSR `0xffff9fc0`). Otherwise it matches that library: it restores the 32-byte environment and raises the union of the x87 and MXCSR flags pending before the call in both units, as FreeBSD 11.4 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/msun/amd64/fenv.c) does, and an unmasked pending flag traps. `__fe_dfl_env` holds the 32 bytes that library exports: x87 control `0xffff037f`, status `0xffff0000`, tag `0xffffffff`, `0xffff` at offset 26 and MXCSR `0x9fc0`. State belongs to the current host thread From 762556eb2d9f4ca729c00b8d3dedab3fd09e2897 Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:27:34 +0100 Subject: [PATCH 236/702] feat(libc): implement signed 32-bit minimum value helper --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/VectorAlgorithms.cpp | 15 +++++++++ core/libs/tests/GuestVectorAlgorithms.cpp | 36 +++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 53 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 441d680159..d83f265905 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -1114,6 +1114,7 @@ const void* APS5_VABI __std_min_element_1_nid_postfix(const void* first, const v const void* APS5_VABI __std_min_element_2_nid_postfix(const void* first, const void* last, bool isSigned); const void* APS5_VABI __std_min_element_4_nid_postfix(const void* first, const void* last, bool isSigned); const void* APS5_VABI __std_min_element_8_nid_postfix(const void* first, const void* last, bool isSigned); +std::int32_t APS5_VABI __std_min_4i_nid_postfix(const void* first, const void* last); const void* APS5_VABI __std_max_element_1_nid_postfix(const void* first, const void* last, bool isSigned); const void* APS5_VABI __std_max_element_2_nid_postfix(const void* first, const void* last, bool isSigned); const void* APS5_VABI __std_max_element_4_nid_postfix(const void* first, const void* last, bool isSigned); diff --git a/core/libs/prx/libc/src/VectorAlgorithms.cpp b/core/libs/prx/libc/src/VectorAlgorithms.cpp index 03b7a62dd3..d94c01537f 100644 --- a/core/libs/prx/libc/src/VectorAlgorithms.cpp +++ b/core/libs/prx/libc/src/VectorAlgorithms.cpp @@ -139,6 +139,21 @@ const void* APS5_VABI __std_min_element_1_nid_postfix(const void* first, const v const void* APS5_VABI __std_min_element_2_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint16_t>(first, last, isSigned, true, false).minimum; } const void* APS5_VABI __std_min_element_4_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint32_t>(first, last, isSigned, true, false).minimum; } const void* APS5_VABI __std_min_element_8_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint64_t>(first, last, isSigned, true, false).minimum; } + +std::int32_t APS5_VABI __std_min_4i_nid_postfix(const void* first, const void* last) { + const auto beginAddress = reinterpret_cast<std::uintptr_t>(first); + const auto endAddress = reinterpret_cast<std::uintptr_t>(last); + if (!first || !last || endAddress <= beginAddress || (endAddress - beginAddress) % sizeof(std::int32_t) != 0) NotImplemented_nid_no_patch(__func__); + auto* current = static_cast<const unsigned char*>(first); + const auto* end = static_cast<const unsigned char*>(last); + std::int32_t minimum = Read<std::int32_t>(current); + for (current += sizeof(std::int32_t); current != end; current += sizeof(std::int32_t)) { + const auto value = Read<std::int32_t>(current); + if (value < minimum) minimum = value; + } + return minimum; +} + const void* APS5_VABI __std_max_element_1_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint8_t>(first, last, isSigned, false, true).maximum; } const void* APS5_VABI __std_max_element_2_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint16_t>(first, last, isSigned, false, true).maximum; } const void* APS5_VABI __std_max_element_4_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign<std::uint32_t>(first, last, isSigned, false, true).maximum; } diff --git a/core/libs/tests/GuestVectorAlgorithms.cpp b/core/libs/tests/GuestVectorAlgorithms.cpp index d9c21878c3..cb5f524251 100644 --- a/core/libs/tests/GuestVectorAlgorithms.cpp +++ b/core/libs/tests/GuestVectorAlgorithms.cpp @@ -155,6 +155,42 @@ static void CheckMinMax() { Require(single.minimum == wides && single.maximum == wides); const MinMaxPointers empty = __std_minmax_element_8_nid_postfix(wides, wides, false); Require(empty.minimum == wides && empty.maximum == wides); + + const std::int32_t edges[] = {INT32_MIN, INT32_MIN + 1, -0x10001, -0x10000, -1, 0, 1, 0xffff, 0x10000, INT32_MAX - 1, INT32_MAX}; + for (const auto& left : edges) { + Require(__std_min_4i_nid_postfix(&left, &left + 1) == left); + for (const auto& right : edges) { + const std::int32_t pair[] = {left, right}; + Require(__std_min_4i_nid_postfix(pair, pair + 2) == std::min(left, right)); + } + } + std::uint32_t state = 0xa9f28537; + for (const std::size_t count : {1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 257}) { + std::vector<std::int32_t> values(count); + for (auto& value : values) { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + std::memcpy(&value, &state, sizeof(value)); + } + const auto expected = *std::min_element(values.begin(), values.end()); + for (std::size_t offset = 0; offset < 32; ++offset) { + std::vector<unsigned char> storage(count * sizeof(std::int32_t) + 64, 0xff); + std::memcpy(storage.data() + offset, values.data(), count * sizeof(std::int32_t)); + const auto original = storage; + Require(__std_min_4i_nid_postfix(storage.data() + offset, storage.data() + offset + count * sizeof(std::int32_t)) == expected); + Require(storage == original); + } + std::fill(values.begin(), values.end(), INT32_MAX); + Require(__std_min_4i_nid_postfix(values.data(), values.data() + count) == INT32_MAX); + for (std::size_t position = 0; position < count; ++position) { + values[position] = -0x10005; + Require(__std_min_4i_nid_postfix(values.data(), values.data() + count) == -0x10005); + values[position] = INT32_MAX; + } + } + const std::int32_t repeated[] = {-9, -9, -9, -9}; + Require(__std_min_4i_nid_postfix(repeated, repeated + 4) == -9); } static void CheckReverseAndSwap() { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..5e3f67410a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -29,6 +29,7 @@ - [feupdateenv, __fe_dfl_env](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `YXQ4gXamCrY`) - a null environment or MXCSR bits the host's `FXSAVE` mask does not support (zero mask: `0xffbf`) throw before changing state, where the system `libSceLibcInternal` of PS5 firmware 13.60 faults (`SIGSEGV`, and `SIGBUS` for MXCSR `0xffff9fc0`). Otherwise it matches that library: it restores the 32-byte environment and raises the union of the x87 and MXCSR flags pending before the call in both units, as FreeBSD 11.4 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/msun/amd64/fenv.c) does, and an unmasked pending flag traps. `__fe_dfl_env` holds the 32 bytes that library exports: x87 control `0xffff037f`, status `0xffff0000`, tag `0xffffffff`, `0xffff` at offset 26 and MXCSR `0x9fc0`. State belongs to the current host thread - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws. The console's low-address layout is not measured. Linux and Windows reserve free addresses from 64 KiB, except host mappings and low chunks the Linux address policy refuses; null hints still allocate from 8 GiB. macOS and ASan builds still start the arena at 8 GiB and cannot supply low 32-bit addresses. Guest thread creation ignores a stack address set in its attributes. A host-loaded executable is not moved, so a PIE above the arena gets no memory within 2 GiB of its code ([#5217](https://github.com/boykopovar/AnyPS5/issues/5217)) - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error +- [__std_min_4i](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `lpMzqv5j5HY`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, signed 32-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x624d0`, size `0x1a2`, read from disassembly and run directly on the host, not measured on a console. - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` writes to the guest's standard output what a payload process on a PS5 (firmware 13.60) wrote: `[SceLibc] A heap error is detected.`, `[SceLibc] -- Mspace Name --`, then the C string at heap+0x3B0 followed by ` (%2u)` of the 32-bit value at heap+0x3F4 when it is not 0, the reason `segment out of range.` for error 1 only (38 other measured values print none), and `pid:<0x..> tid:<0x..>`, and returns. The pid and tid are the host's, where the console prints its own process and kernel thread ids. The block's contents did not change the console's output and it is not read here; the values and the guest return address also go to the host's standard error. A null heap faulted on the console and throws here. What the other heap fields and error values mean is unknown - [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. - [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` From 5e1fc33a2619cbfeda8b0440d7f6d52a0b426538 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:31:21 +0000 Subject: [PATCH 237/702] feat(libSceNpTus): declare a stub with its shadPS4 signature They still throw through NotImplemented_nid_no_patch; only the parameter lists change. The only source is shadPS4's PS4 prototypes; nothing is measured on a PS5. --- core/libs/prx/libSceNpTus/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index 2f632e6d18..fcd25e7083 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -1001,7 +1001,7 @@ int APS5_VABI sceNpTssGetData() { return 0; } -int APS5_VABI sceNpTusCreateNpTitleCtx() { +int APS5_VABI sceNpTusCreateNpTitleCtx(std::uint32_t serviceLabel, void* npId) { NotImplemented_nid_no_patch(__func__); return 0; } From 34e039ab3ee50ea7254eee38e4e4e5fbef83e2c6 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:31:21 +0000 Subject: [PATCH 238/702] docs(libSceNpTus): record the single-source shadPS4 signatures of the declared stubs --- docs/dev/debt/libSceNpTus.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md index 142eda2d11..4e34884163 100644 --- a/docs/dev/debt/libSceNpTus.md +++ b/docs/dev/debt/libSceNpTus.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceNpTusCreateNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) - throws through `NotImplemented_nid_no_patch`; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. - [TUS variable outputs](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layouts of the variable arrays of the `sceNpTusGetMultiSlotVariable*` calls and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` are not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked; shadPS4, and RPCS3 in its PS3 `sceNpTusGetMultiSlotVariable*` and `sceNpTusAddAndGetVariable*` calls, return `INVALID_ALIGNMENT` unless the size is `arrayLen` variables of the call's type, or one variable for `outVariable`. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. - [Multi-slot variable calls](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the multi-slot get, set and delete calls are not measured on a console; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_tus/np_tus.cpp), where they differ between functions: `arrayLen < 1` is `INSUFFICIENT_ARGUMENT` in the NpId and virtual-user delete calls and in `sceNpTusGetMultiSlotVariableVUserAsync`, and `INVALID_ARGUMENT` in the others; a null NpId is accepted except in delete; an empty virtual user ID is `INVALID_ARGUMENT`. RPCS3's [PS3 module](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/Cell/Modules/sceNpTus.cpp) has only the NpId and virtual-user calls; it returns `INVALID_ARGUMENT` for an empty array in all of them, rejects a null target in get and set and does not check for an empty virtual user ID. - [Variable, data, data status and TSS requests](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the 82 add-and-get, try-and-set, data, data status, multi-user, friends and `sceNpTssGetSmallStorage` calls are not measured on a console; their signatures, checks and order follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_tus/np_tus.cpp), including its differences between forms: the NpId try-and-set calls return `INVALID_ARGUMENT` for an operation type outside 1-6 where the others return `INVALID_TYPE`, and the NpId and virtual-user multi-slot data status calls return `TOO_MANY_NPID` for more than 64 slots. As for the calls above, the output and structure sizes are not checked, and every request finishes with `SCE_NP_ERROR_SIGNED_OUT` From da573bb3a3657b0c98f9b4decde73d17273d091d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:05:54 +0000 Subject: [PATCH 239/702] fix(libSceNpTus): give sceNpTusCreateNpTitleCtx its signature in the public header main declares it with an empty parameter list in Api.hpp, which conflicts with the definition. --- core/libs/prx/libSceNpTus/include/Api.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpTus/include/Api.hpp b/core/libs/prx/libSceNpTus/include/Api.hpp index eacd0fe74a..79f4b14588 100644 --- a/core/libs/prx/libSceNpTus/include/Api.hpp +++ b/core/libs/prx/libSceNpTus/include/Api.hpp @@ -137,7 +137,7 @@ int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(int reqId, std::uint64_t tar int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); int APS5_VABI sceNpTusTryAndSetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); int APS5_VABI sceNpTssGetData(); -int APS5_VABI sceNpTusCreateNpTitleCtx(); +int APS5_VABI sceNpTusCreateNpTitleCtx(std::uint32_t serviceLabel, void* npId); int APS5_VABI sceNpTusGetData(int reqId, const void* targetNpId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); int APS5_VABI sceNpTusGetDataVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); int APS5_VABI sceNpTusGetMultiSlotVariable(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); From de459c7ed8238a6b78d566e539d030bd7cef14d3 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 20:09:44 -0500 Subject: [PATCH 240/702] test(libSceNet): cover bad file descriptor rejection in socket close --- core/libs/tests/GuestDatagramReceive.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestDatagramReceive.cpp b/core/libs/tests/GuestDatagramReceive.cpp index d11fe04de6..4ee3e106a3 100644 --- a/core/libs/tests/GuestDatagramReceive.cpp +++ b/core/libs/tests/GuestDatagramReceive.cpp @@ -144,7 +144,14 @@ int main(int argc, char** argv) { Require(argc == 2, "expected receive API argument"); const bool net = std::strncmp(argv[1], "sce", 3) == 0; const bool from = std::strstr(argv[1], "from") != nullptr; - if (net) Require(sceNetInit_nid_postfix() == 0, "initialize libSceNet"); + if (net) { + Require(sceNetInit_nid_postfix() == 0, "initialize libSceNet"); + Require(sceNetSocketClose(-1) < 0, "reject invalid descriptor in sceNetSocketClose"); + Require(*sceNetErrnoLoc() == 9, "bad file descriptor error"); + } else { + Require(close_nid_postfix(-1) < 0, "reject invalid descriptor in close"); + Require(*__error_nid_postfix() == 9, "bad file descriptor error"); + } Check(net, from, 2); Check(net, from, 28); } From 260454d8b933ff3bf9696ad8dcd2afb62131437f Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 20:11:55 -0500 Subject: [PATCH 241/702] test(libkernel): cover invalid usec and bad descriptor validation in futimes --- core/libs/tests/GuestFileTimestamps.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 8a081181a5..b83e664530 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -263,6 +263,9 @@ int main() { #ifdef _WIN32 Require(futimes_nid_postfix(pipe[0], first) == -1 && *__error_nid_postfix() == 22, "pipe timestamp update fails"); #endif + const KernelTimeval invalidUsec[2] = {{first[0].tv_sec, -1}, {first[1].tv_sec, 0}}; + Require(futimes_nid_postfix(pipe[0], invalidUsec) == -1 && *__error_nid_postfix() == 22, "negative usec rejected in futimes"); + Require(futimes_nid_postfix(-1, first) == -1 && *__error_nid_postfix() == 9, "bad file descriptor in futimes"); Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0, "close pipe"); const auto throughFile = (root / "file.txt" / "child").string(); for (const auto update : {utimes_nid_postfix, lutimes_nid_postfix}) { From 218e9c7e4e5be8f45246ea159707e3f22344adab Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Tue, 27 Oct 2026 02:16:43 +0100 Subject: [PATCH 242/702] feat(libc): add standard count-ones functions --- core/libs/prx/libc/CMakeLists.txt | 12 ++++ core/libs/prx/libc/include/Api.hpp | 5 ++ core/libs/prx/libc/src/IntegerBuiltins.cpp | 6 ++ core/libs/tests/GuestCountOnes.cpp | 68 ++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 5 files changed, 92 insertions(+) create mode 100644 core/libs/tests/GuestCountOnes.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 68763d4139..d182409d7d 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -447,6 +447,18 @@ if(BUILD_TESTING) set_tests_properties(guest_random_device_seed PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test_executable(guest_count_ones_tests ${LIBS_INCLUDE_DIR}/tests/GuestCountOnes.cpp) + target_include_directories(guest_count_ones_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_count_ones_tests PRIVATE libc) + configure_windows_unwind(guest_count_ones_tests) + foreach(width uc us ui ul ull) + add_test(NAME guest_count_ones_${width} COMMAND guest_count_ones_tests ${width}) + set_tests_properties(guest_count_ones_${width} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_count_ones_${width} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() + endforeach() add_test_executable(guest_integer_builtins_tests ${LIBS_INCLUDE_DIR}/tests/GuestIntegerBuiltins.cpp) target_include_directories(guest_integer_builtins_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_integer_builtins_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 441d680159..2b83faaaa9 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -420,6 +420,11 @@ int APS5_VABI __parityti2_nid_postfix(Uint128 value); int APS5_VABI __popcountsi2_nid_postfix(std::uint32_t value); int APS5_VABI __popcountdi2_nid_postfix(std::uint64_t value); int APS5_VABI __popcountti2_nid_postfix(Uint128 value); +std::uint32_t APS5_VABI stdc_count_ones_uc_nid_postfix(std::uint8_t value); +std::uint32_t APS5_VABI stdc_count_ones_us_nid_postfix(std::uint16_t value); +std::uint32_t APS5_VABI stdc_count_ones_ui_nid_postfix(std::uint32_t value); +std::uint32_t APS5_VABI stdc_count_ones_ul_nid_postfix(std::uint64_t value); +std::uint32_t APS5_VABI stdc_count_ones_ull_nid_postfix(std::uint64_t value); int APS5_VABI __cmpdi2_nid_postfix(std::int64_t a, std::int64_t b); int APS5_VABI __cmpti2_nid_postfix(Int128 a, Int128 b); int APS5_VABI __ucmpdi2_nid_postfix(std::uint64_t a, std::uint64_t b); diff --git a/core/libs/prx/libc/src/IntegerBuiltins.cpp b/core/libs/prx/libc/src/IntegerBuiltins.cpp index 44a6e6df13..0513e36a21 100644 --- a/core/libs/prx/libc/src/IntegerBuiltins.cpp +++ b/core/libs/prx/libc/src/IntegerBuiltins.cpp @@ -226,6 +226,12 @@ int APS5_VABI __popcountti2_nid_postfix(Uint128 value) { return std::popcount(High(value)) + std::popcount(Low(value)); } +std::uint32_t APS5_VABI stdc_count_ones_uc_nid_postfix(std::uint8_t value) { return std::popcount(value); } +std::uint32_t APS5_VABI stdc_count_ones_us_nid_postfix(std::uint16_t value) { return std::popcount(value); } +std::uint32_t APS5_VABI stdc_count_ones_ui_nid_postfix(std::uint32_t value) { return std::popcount(value); } +std::uint32_t APS5_VABI stdc_count_ones_ul_nid_postfix(std::uint64_t value) { return std::popcount(value); } +std::uint32_t APS5_VABI stdc_count_ones_ull_nid_postfix(std::uint64_t value) { return std::popcount(value); } + int APS5_VABI __cmpdi2_nid_postfix(std::int64_t a, std::int64_t b) { return Compare(a, b); } int APS5_VABI __cmpti2_nid_postfix(Int128 a, Int128 b) { return Compare(a, b); } int APS5_VABI __ucmpdi2_nid_postfix(std::uint64_t a, std::uint64_t b) { return Compare(a, b); } diff --git a/core/libs/tests/GuestCountOnes.cpp b/core/libs/tests/GuestCountOnes.cpp new file mode 100644 index 0000000000..55265b826f --- /dev/null +++ b/core/libs/tests/GuestCountOnes.cpp @@ -0,0 +1,68 @@ +#include "prx/libc/include/Api.hpp" +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <limits> + +static void Require(bool condition) { + if (condition) return; + std::fprintf(stderr, "count ones mismatch\n"); + std::abort(); +} + +static std::uint32_t Reference(std::uint64_t value) { + std::uint32_t result = 0; + while (value != 0) { + result += value & 1; + value >>= 1; + } + return result; +} + +template<typename TValue, typename TCounter> +static void Check(TCounter count) { + constexpr unsigned width = std::numeric_limits<TValue>::digits; + const auto maximum = std::numeric_limits<TValue>::max(); + Require(count(0) == 0); + Require(count(maximum) == width); + if constexpr (width <= 16) { + for (std::uint32_t value = 0; value <= maximum; ++value) { + Require(count(static_cast<TValue>(value)) == Reference(value)); + } + } + for (unsigned bit = 0; bit < width; ++bit) { + const auto value = static_cast<TValue>(TValue{1} << bit); + Require(count(value) == 1); + Require(count(static_cast<TValue>(~value)) == width - 1); + for (unsigned other = 0; other < width; ++other) { + const auto pair = static_cast<TValue>(value | (TValue{1} << other)); + Require(count(pair) == (bit == other ? 1u : 2u)); + } + } + if constexpr (width > 16) { + std::uint64_t state = 0x769c81d32f04a5b7ULL; + for (unsigned index = 0; index < 100000; ++index) { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + const auto value = static_cast<TValue>(state); + Require(count(value) == Reference(value)); + Require(count(static_cast<TValue>(~value)) == width - Reference(value)); + } + } +} + +int main(int argc, char** argv) { + Require(argc == 2); + const int saved = *__error_nid_postfix(); + *__error_nid_postfix() = 87; + if (std::strcmp(argv[1], "uc") == 0) Check<std::uint8_t>(stdc_count_ones_uc_nid_postfix); + else if (std::strcmp(argv[1], "us") == 0) Check<std::uint16_t>(stdc_count_ones_us_nid_postfix); + else if (std::strcmp(argv[1], "ui") == 0) Check<std::uint32_t>(stdc_count_ones_ui_nid_postfix); + else if (std::strcmp(argv[1], "ul") == 0) Check<std::uint64_t>(stdc_count_ones_ul_nid_postfix); + else if (std::strcmp(argv[1], "ull") == 0) Check<std::uint64_t>(stdc_count_ones_ull_nid_postfix); + else Require(false); + Require(*__error_nid_postfix() == 87); + *__error_nid_postfix() = saved; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..1d4d5e5801 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -49,6 +49,7 @@ - [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm - [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) - a null state throws where the system `libSceLibcInternal` of PS5 firmware 13.60 faults, `initstate` below 8 bytes skips that library's "not enough state" warning, and concurrent callers (one lock here) and a title's own libc are not measured. Otherwise it follows payload processes on PS5 firmware 12.70 and 13.60: a fresh process draws the `srandom(1)` sequence; `srandom` seeds through `good_rand` (int32 argument, 0 becomes 123459876, `16807 * (x % 127773) - 2836 * (x / 127773)` plus 0x7fffffff when negative); `initstate` picks types 0 to 4 by size, returns null below 8 bytes without touching `errno` but still saves the current rear position; both calls return the previous state; `setstate` takes a rear position past the degree unchecked - [srandomdev](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) is not verified against the console. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NID `a2MOZf++Wjg`; it follows FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c), filling every active generator word from host OS entropy instead of `KERN_ARND`, then resetting additive feedback positions without warming up. An entropy failure throws rather than leaving `sysctl`'s failure unchecked. Calls share the generator lock. +- [stdc_count_ones_uc, us, ui, ul and ull](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) (libc) have not been measured on a console. Their unsigned 8/16/32/64/64-bit arguments and count result follow PPSA03671 v01.001.005's bundled libc.prx, read from disassembly and run directly on a Windows host; the ul argument remains 64-bit on Windows. - [Integer compiler-rt builtins](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) (libc) - the console traps in `__divsi3`, `__modsi3` and the 64- and 128-bit divisions on a zero divisor (`div`) and in `__negv*`, `__absv*` and `__mulv*` on overflow (an abort helper, then `ud2`); these throw instead. As in the `libc.prx` bundled with PPSA08427 (disassembly), `__addv*` and `__subv*` wrap without an overflow check, and `__udivsi3`, `__umodsi3` and `__udivmodsi4` divide in software, giving a quotient of 0 and the dividend as remainder for a zero divisor; `__clzdi2` and `__ctzdi2` of 0 return 32 and `__clzti2` and `__ctzti2` of 0 return 64 - [qsort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows FreeBSD 11 [qsort.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdlib/qsort.c): Bentley and McIlroy's quicksort, with insertion sort below 7 elements and after a partition pass that swaps nothing, so elements that compare equal end in FreeBSD's order and the comparator is called in the same sequence. That the console's libc uses this FreeBSD code is unverified - [memcpy_s, memmove_s, strcpy_s, strncpy_s, strcat_s, strncat_s, wmemcpy_s, wmemmove_s, wcsncpy_s, wcscat_s, wcsncat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 Annex K destination size constraints, with RSIZE_MAX taken as (SIZE_MAX >> 1) / sizeof(char16_t) for wide operations and SIZE_MAX >> 1 for byte operations. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. From f3303edc0d236b3bebcdb7c1f2799433cf48f40a Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 20:38:45 -0500 Subject: [PATCH 243/702] test(libkernel): cover null pointer and invalid clock id in clock gettime and getres --- core/libs/tests/GuestCpuTimeClocks.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestCpuTimeClocks.cpp b/core/libs/tests/GuestCpuTimeClocks.cpp index 18ae60bb87..964264e060 100644 --- a/core/libs/tests/GuestCpuTimeClocks.cpp +++ b/core/libs/tests/GuestCpuTimeClocks.cpp @@ -115,7 +115,10 @@ int main() { Require(sceKernelClockGettime(GUEST_CLOCK_PROCESS_CPUTIME_ID, &time) == SCE_OK); Require(time.tv_sec >= 0 && time.tv_nsec >= 0 && time.tv_nsec < NANOS_PER_SECOND); Require(sceKernelClockGettime(GUEST_CLOCK_THREAD_CPUTIME_ID, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelClockGettime(GUEST_CLOCK_PROCESS_CPUTIME_ID, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelClockGetres(GUEST_CLOCK_THREAD_CPUTIME_ID, nullptr) == SCE_KERNEL_ERROR_EFAULT); Require(sceKernelClockGetres(GUEST_CLOCK_PROCESS_CPUTIME_ID, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelClockGettime(static_cast<KernelClockid>(-1), &time) == SCE_KERNEL_ERROR_EINVAL); RequireResolution(GUEST_CLOCK_THREAD_CPUTIME_ID); RequireResolution(GUEST_CLOCK_PROCESS_CPUTIME_ID); From 3b5e611b36f9122782d8add6b44fa03bee75dc37 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 20:08:42 -0500 Subject: [PATCH 244/702] test(libkernel): cover null thread handle and mask validation in thread affinity --- core/libs/tests/GuestCurrentCpu.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestCurrentCpu.cpp b/core/libs/tests/GuestCurrentCpu.cpp index 323eead874..7a07c2311c 100644 --- a/core/libs/tests/GuestCurrentCpu.cpp +++ b/core/libs/tests/GuestCurrentCpu.cpp @@ -1,3 +1,4 @@ +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "SceTypes.hpp" @@ -35,6 +36,13 @@ int main() { Require(sceKernelGetCpuFrequency() > 0 && static_cast<std::uint64_t>(sceKernelGetCpuFrequency()) == sceKernelGetTscFrequency()); Require(sceKernelGetTscFrequency() >= 100000000u && sceKernelGetTscFrequency() <= 10000000000u); + KernelCpumask currentMask = 0; + Require(scePthreadSetaffinity(nullptr, available) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadSetaffinity(scePthreadSelf(), 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadGetaffinity(nullptr, ¤tMask) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadGetaffinity(scePthreadSelf(), nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadGetaffinity(scePthreadSelf(), ¤tMask) == SCE_OK && currentMask != 0); + const std::array<KernelCpumask, 8> affinities{available, 0x1FFB, 0x3, 0x3F, 0x1000, 0x1, 0xFFFF, 0x10000}; for (const KernelCpumask affinity : affinities) { const KernelCpumask expected = (affinity & available) != 0 ? affinity & available : available; From 25a0e0c4fe0bec10342cc839950d86032393523e Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 20:10:42 -0500 Subject: [PATCH 245/702] test(libkernel): cover double close and invalid file descriptor rejection in sceKernelClose --- core/libs/tests/GuestDirectoryRewind.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp index 424c5bda23..2343ebfe37 100644 --- a/core/libs/tests/GuestDirectoryRewind.cpp +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -1,3 +1,4 @@ +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -144,5 +145,7 @@ int main() { Require(sceKernelLseek(reused, 0, 1) == 5); Require(sceKernelLseek(reused, 0, 2) == 0); Require(sceKernelClose(reused) == 0); + Require(sceKernelClose(reused) == SCE_KERNEL_ERROR_EBADF); + Require(sceKernelClose(-1) == SCE_KERNEL_ERROR_EBADF); std::filesystem::remove_all(root); } From 1220ef855674e59cb8420ae393e42a61b8e62a93 Mon Sep 17 00:00:00 2001 From: Himansh Raj <iamthehimansh@gmail.com> Date: Tue, 27 Oct 2026 07:26:38 +0530 Subject: [PATCH 246/702] test(relinker): check that codesign leaves the macOS entry stub intact --- .../relinker/tests/test_macos_output.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/tests/test_macos_output.py b/core/relinker/relinker/tests/test_macos_output.py index fd9407a851..2330e9d368 100644 --- a/core/relinker/relinker/tests/test_macos_output.py +++ b/core/relinker/relinker/tests/test_macos_output.py @@ -29,6 +29,7 @@ PAGE = 0x1000 IMAGE_BASE = 0x140000000 META_VERSION = 2 PROT_EXEC = 4 +CODE_SIGNATURE_RESERVE = 0x400 FS_LOAD = bytes.fromhex("64488b0425" "00000000") GS_LOAD = bytes.fromhex("65488b04c5" "00000000") @@ -108,6 +109,7 @@ def check_structure(name, data): linkedit = macho.segment("__LINKEDIT") check(linkedit["initprot"] == 1 and linkedit["fileoff"] + linkedit["filesize"] == len(data), f"{name}: __LINKEDIT does not end the file") check(macho.entryoff is not None and any(lo <= macho.entryoff < hi for lo, hi in macho.executable_ranges()), f"{name}: entry point is outside the executable segments") + check(macho.entryoff >= macho.end + CODE_SIGNATURE_RESERVE, f"{name}: entry stub at {macho.entryoff:#x} leaves under {CODE_SIGNATURE_RESERVE:#x} bytes after the load commands for codesign") check("@rpath/libkernel.prx" in macho.dylibs and "/usr/lib/libSystem.B.dylib" in macho.dylibs, f"{name}: dependencies {macho.dylibs}") check(macho.rpaths == ["@executable_path/libs"], f"{name}: run paths {macho.rpaths}") check(macho.dyld_info is not None, f"{name}: no binding information") @@ -143,6 +145,18 @@ def check_imports(data): check(bytes.fromhex("4883ec08") in code, "import: the guest code is missing from the image") +def check_signed_entry(directory, data): + if sys.platform != "darwin": + return + signed = Path(directory) / "import-signed" + signed.write_bytes(data) + result = subprocess.run(["codesign", "-s", "-", "-f", str(signed)], capture_output=True, text=True, timeout=60) + check(result.returncode == 0, f"codesign failed: {result.stdout}{result.stderr}") + entryoff = MachO(data).entryoff + after = signed.read_bytes() + check(after[entryoff:entryoff + 64] == data[entryoff:entryoff + 64], "codesign overwrote the entry stub") + + def check_runtime_dependencies(relinker, directory): for needed, expected in ( (b"libc.prx", ["@rpath/libc.prx", "@rpath/libkernel.prx"]), @@ -208,7 +222,9 @@ def check_runpath_lists(relinker, directory): def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-macos-output-") as directory: - check_imports(relink(relinker, directory, "import", import_fixture())) + imported = relink(relinker, directory, "import", import_fixture()) + check_imports(imported) + check_signed_entry(directory, imported) check_runtime_dependencies(relinker, directory) check_tls(relink(relinker, directory, "tls", tls_fixture())) check_options(relinker, directory) From 051d00589c2b9a604ce5a3d7d25ec3f694b994ac Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:17:53 +0800 Subject: [PATCH 247/702] fix(tools): preserve annotation input modules --- tools/annotate_module.py | 5 +++ tools/tests/test_annotate_module.py | 68 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/tools/annotate_module.py b/tools/annotate_module.py index b57b34178a..a4ef16d003 100644 --- a/tools/annotate_module.py +++ b/tools/annotate_module.py @@ -268,6 +268,11 @@ def export_lines(exports, thread_local, namer): def annotate(module_path, prefix, namer): + source = Path(module_path) + for suffix in (".imports.txt", ".exports.txt", ".asm"): + output = Path(prefix + suffix) + if output.resolve() == source.resolve() or (output.exists() and source.exists() and output.samefile(source)): + raise ModuleError(f"annotation output must be separate from the input module: {output}") module = Module(Path(module_path).read_bytes()) slots = module.imports() exports, thread_local = module.exports() diff --git a/tools/tests/test_annotate_module.py b/tools/tests/test_annotate_module.py index bb01a2b717..2b819a2d80 100644 --- a/tools/tests/test_annotate_module.py +++ b/tools/tests/test_annotate_module.py @@ -1,4 +1,5 @@ import contextlib +import os import io import re import shutil @@ -255,5 +256,72 @@ class AnnotateModuleTests(unittest.TestCase): self.assert_fails(build_module(machine=0xB7), "not an x86-64 ELF") +class InputProtectionTests(unittest.TestCase): + def test_cli_rejects_alias_without_success_or_partial_reports(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + module = root / "report.exports.txt" + original = build_module() + module.write_bytes(original) + result = subprocess.run([sys.executable, annotate_module.__file__, str(module), "--out", str(root / "report"), "--no-names"], + capture_output=True, text=True, timeout=10) + self.assertNotEqual(result.returncode, 0) + self.assertIn("separate from the input", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + self.assertEqual(module.read_bytes(), original) + self.assertFalse((root / "report.imports.txt").exists()) + self.assertFalse((root / "report.asm").exists()) + + def test_all_report_outputs_are_checked_before_any_write(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for suffix in (".imports.txt", ".exports.txt", ".asm"): + with self.subTest(suffix=suffix): + prefix = root / suffix[1:].replace(".", "_") + module = Path(str(prefix) + suffix) + original = build_module() + module.write_bytes(original) + with self.assertRaisesRegex(annotate_module.ModuleError, "separate from the input"): + annotate_module.annotate(str(module), str(prefix), annotate_module.Namer({})) + self.assertEqual(module.read_bytes(), original) + for other in (".imports.txt", ".exports.txt", ".asm"): + if other != suffix: + self.assertFalse(Path(str(prefix) + other).exists()) + + def test_hardlinked_outputs_preserve_the_input_and_existing_reports(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + module = root / "input.prx" + original = build_module() + module.write_bytes(original) + prefix = root / "report" + first = root / "report.imports.txt" + first.write_bytes(b"previous report") + os.link(module, root / "report.asm") + with self.assertRaisesRegex(annotate_module.ModuleError, "separate from the input"): + annotate_module.annotate(str(module), str(prefix), annotate_module.Namer({})) + self.assertEqual(module.read_bytes(), original) + self.assertEqual(first.read_bytes(), b"previous report") + self.assertFalse((root / "report.exports.txt").exists()) + + def test_symlinked_output_is_rejected_where_available(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + module = root / "input.prx" + original = build_module() + module.write_bytes(original) + output = root / "report.asm" + try: + output.symlink_to(module) + except OSError as error: + if os.name == "nt" and getattr(error, "winerror", None) == 1314: + self.skipTest("Windows symbolic-link privileges unavailable") + raise + with self.assertRaisesRegex(annotate_module.ModuleError, "separate from the input"): + annotate_module.annotate(str(module), str(root / "report"), annotate_module.Namer({})) + self.assertEqual(module.read_bytes(), original) + + if __name__ == "__main__": unittest.main() From 3a1f390b31f6a98e793c290bcc0a557134b23683 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:17:55 +0800 Subject: [PATCH 248/702] fix(tools): select matching sampled module candidates --- tools/symbolize_samples.py | 29 +++++++++++++- tools/tests/test_symbolize_samples.py | 57 ++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/tools/symbolize_samples.py b/tools/symbolize_samples.py index 1536075235..c4b60d6c74 100644 --- a/tools/symbolize_samples.py +++ b/tools/symbolize_samples.py @@ -128,7 +128,7 @@ class Symbolizer: for directory in libs: for path in Path(directory).iterdir(): if path.is_file(): - self.files.setdefault(path.name.lower(), path) + self.files.setdefault(path.name.lower(), {}).setdefault(path.resolve(), path) self.headers = headers self.modules = {} self.demangle = demangle and shutil.which("c++filt") is not None @@ -136,11 +136,36 @@ class Symbolizer: self.warn = warn self.unverified = unverified + def candidate_path(self, name): + paths = list(self.files.get(name.lower(), {}).values()) + if len(paths) <= 1: + return paths[0] if paths else None + identity = self.headers.get(name) + if identity is None and not self.unverified: + self.warn(f"{name}: the samples have no '# module' record; multiple candidates stay unsymbolized") + return None + matches = [] + for path in paths: + try: + candidate = PeModule(path) + except (OSError, ValueError, struct.error) as error: + self.warn(f"{name}: {path}: {error}") + continue + if identity is None or identity == (candidate.size, candidate.timestamp): + matches.append(path) + if len(matches) == 1: + return matches[0] + if matches: + self.warn(f"{name}: ambiguous module candidates; matching size and timestamp do not identify one file, so frames stay unsymbolized") + else: + self.warn(f"{name}: no module candidate matches the sampled size and timestamp; frames stay unsymbolized") + return None + def module(self, name): if name in self.modules: return self.modules[name] module = None - path = self.files.get(name.lower()) + path = self.candidate_path(name) if path is not None: try: module = PeModule(path) diff --git a/tools/tests/test_symbolize_samples.py b/tools/tests/test_symbolize_samples.py index 2393d66480..7d29b07df8 100644 --- a/tools/tests/test_symbolize_samples.py +++ b/tools/tests/test_symbolize_samples.py @@ -17,7 +17,7 @@ def symbol(name, value, section, kind, storage, aux=0): return struct.pack("<8sIhHBB", name, value, section, kind, storage, aux) -def build_pe(path, with_symbols=True, with_exports=False, second_code=False): +def build_pe(path, with_symbols=True, with_exports=False, second_code=False, timestamp=TIMESTAMP): image = bytearray(0xC00) image[0:2] = b"MZ" struct.pack_into("<I", image, 0x3C, 0x40) @@ -35,7 +35,7 @@ def build_pe(path, with_symbols=True, with_exports=False, second_code=False): symbol(b"data_obj", 0x08, 2, 0, 2), ] + ([symbol(b"late_fn", 0x40, 3, 0x20, 2)] if second_code else [])) symbol_count = len(symbols) // 18 - struct.pack_into("<HHIIIHH", image, 0x44, 0x8664, 3 if second_code else 2, TIMESTAMP, 0x800 if with_symbols else 0, symbol_count, 240, 0x22) + struct.pack_into("<HHIIIHH", image, 0x44, 0x8664, 3 if second_code else 2, timestamp, 0x800 if with_symbols else 0, symbol_count, 240, 0x22) optional = 0x58 struct.pack_into("<H", image, optional, 0x20B) struct.pack_into("<Q", image, optional + 24, 0x140000000) @@ -230,5 +230,58 @@ class SymbolizeSamplesTests(unittest.TestCase): symbolize_samples.load(bad) +class CandidateModuleTests(unittest.TestCase): + def setUp(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.root = Path(directory.name) + self.old = self.root / "old" + self.current = self.root / "current" + self.old.mkdir() + self.current.mkdir() + build_pe(self.old / "example.prx", timestamp=TIMESTAMP + 1) + build_pe(self.current / "example.prx") + self.headers = {"example.prx": (SIZE_OF_IMAGE, TIMESTAMP)} + self.frame = "example.prx+0x1010" + + def test_unique_matching_candidate_is_independent_of_directory_order(self): + for directories in ((self.old, self.current), (self.current, self.old)): + with self.subTest(directories=directories): + symbolizer = symbolize_samples.Symbolizer(directories, self.headers, warn=lambda message: None) + self.assertEqual(symbolizer.frame(self.frame), "example.prx!short") + + def test_multiple_matching_candidates_stay_unsymbolized(self): + build_pe(self.old / "example.prx") + data = (self.old / "example.prx").read_bytes().replace(b"short\0", b"other\0") + (self.old / "example.prx").write_bytes(data) + warnings = [] + symbolizer = symbolize_samples.Symbolizer([self.old, self.current], self.headers, warn=warnings.append) + self.assertEqual(symbolizer.frame(self.frame), self.frame) + self.assertIn("ambiguous", "\n".join(warnings)) + count = len(warnings) + self.assertEqual(symbolizer.frame(self.frame), self.frame) + self.assertEqual(len(warnings), count) + + def test_invalid_candidate_does_not_hide_the_matching_module(self): + (self.old / "example.prx").write_bytes(b"not a PE module") + symbolizer = symbolize_samples.Symbolizer([self.old, self.current], self.headers, warn=lambda message: None) + self.assertEqual(symbolizer.frame(self.frame), "example.prx!short") + + def test_repeated_directory_does_not_create_ambiguity(self): + symbolizer = symbolize_samples.Symbolizer([self.current, self.current / "."], self.headers) + self.assertEqual(symbolizer.frame(self.frame), "example.prx!short") + + def test_module_filename_lookup_remains_case_insensitive(self): + name = "EXAMPLE.PRX" + symbolizer = symbolize_samples.Symbolizer([self.old, self.current], {name: (SIZE_OF_IMAGE, TIMESTAMP)}) + self.assertEqual(symbolizer.frame(name + "+0x1010"), name + "!short") + + def test_missing_identity_does_not_choose_between_different_files(self): + for unverified in (False, True): + with self.subTest(unverified=unverified): + symbolizer = symbolize_samples.Symbolizer([self.old, self.current], {}, unverified=unverified, warn=lambda message: None) + self.assertEqual(symbolizer.frame(self.frame), self.frame) + + if __name__ == "__main__": unittest.main() From dcb1013c482aaeddc84ec8c366e35d987c807ee2 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:17:56 +0800 Subject: [PATCH 249/702] fix(tools): reject dangling test graph targets --- CMakeLists.txt | 12 ++++++++++ tools/select_tests.py | 3 +++ tools/tests/test_select_tests.py | 38 ++++++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6aecb54402..ee1616649e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -229,6 +229,18 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py) add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py) + set(select_tests_source "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${select_tests_source}") + file(STRINGS "${select_tests_source}" select_tests_classes REGEX "^class [A-Za-z0-9_]*Tests\\(") + foreach(test_class IN LISTS select_tests_classes) + string(REGEX REPLACE "^class ([A-Za-z0-9_]+).*" "\\1" test_class "${test_class}") + if(test_class STREQUAL "SelectTests") + set_tests_properties(select_tests PROPERTIES ENVIRONMENT "ANYPS5_SELECT_TEST_CLASS=SelectTests" TIMEOUT 30) + else() + add_test(NAME select_tests_${test_class} COMMAND ${Python3_EXECUTABLE} "${select_tests_source}" ${test_class}) + set_tests_properties(select_tests_${test_class} PROPERTIES TIMEOUT 30) + endif() + endforeach() add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedTests) add_test(NAME configure_seed_paths COMMAND ${Python3_EXECUTABLE} diff --git a/tools/select_tests.py b/tools/select_tests.py index 1fa013a3a9..ac80143179 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -183,6 +183,9 @@ def load_graph(path): for name, paths in graph["closures"].items(): if not isinstance(name, str) or not isinstance(paths, list) or any(not isinstance(path, str) for path in paths): raise ValueError(f"{path}: invalid test graph closure {name!r}") + for name, test in graph["tests"].items(): + if test["target"] is not None and test["target"] not in graph["closures"]: + raise ValueError(f"{path}: invalid test graph entry {name!r}: missing closure for {test['target']!r}") return graph diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index cbe8248990..f186aa0856 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -33,7 +33,7 @@ PROJECT = { @unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") -class SelectTests(unittest.TestCase): +class SelectFixture(unittest.TestCase): @classmethod def setUpClass(cls): cls.directory = tempfile.TemporaryDirectory() @@ -76,6 +76,7 @@ class SelectTests(unittest.TestCase): check=True, capture_output=True, text=True).stdout return output, set(re.findall(r"Test +#\d+: (\S+)", shown)) +class SelectTests(SelectFixture): def test_header_selects_the_tests_of_its_executable(self): self.assertEqual(self.select("alpha/alpha.h")[1], {"alpha", "alpha_data", "script"}) @@ -101,6 +102,7 @@ class SelectTests(unittest.TestCase): self.assertIsNone(selected) self.assertIn(changed, output) +class SelectGraphStateTests(SelectFixture): def test_changed_list_with_a_byte_order_mark_and_crlf_keeps_its_first_path(self): listed = self.build / "changed bom.txt" listed.write_bytes(b"\xef\xbb\xbfCMakeLists.txt\r\nalpha/alpha.h\r\n") @@ -133,6 +135,7 @@ class SelectTests(unittest.TestCase): older.write_text(json.dumps(graph), encoding="utf-8") self.assertEqual(self.select("alpha/alpha.h", graph=older)[1], {"alpha", "alpha_data", "beta", "beta_setup", "script"}) +class SelectBuildTests(SelectFixture): def test_selected_target_skips_unselected_executables(self): subprocess.run(["ninja", "-C", str(self.build), "-t", "clean"], check=True, capture_output=True) self.select("alpha/main.c") @@ -171,6 +174,7 @@ class SelectTests(unittest.TestCase): subprocess.run(["cmake", "-S", str(self.source), "-B", str(self.build)], check=True, capture_output=True) subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) +class SelectArgumentTests(SelectFixture): def test_script_test_builds_the_executable_it_gets_as_a_definition(self): cmake, script = self.source / "alpha/CMakeLists.txt", self.source / "data/run.cmake" original = cmake.read_text(encoding="utf-8") @@ -575,5 +579,35 @@ class MissingCommandTests(unittest.TestCase): self.assertEqual(err.errors, "backslashreplace") +class GraphClosureTests(unittest.TestCase): + def test_dangling_targets_fail_instead_of_selecting_no_tests(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + graph = {"tests": {"unit": {"target": "unit", "registered": ["CMakeLists.txt"], + "arguments": [], "sets": [], "requires": [], "depends": []}}, "closures": {}} + path = root / "graph.json" + path.write_text(json.dumps(graph), encoding="utf-8") + changed = root / "changed.txt" + changed.write_text("src/unit.cpp\n", encoding="utf-8") + result = subprocess.run([sys.executable, str(Path(select_tests.__file__)), "explain", str(path), str(changed)], + capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("unit", result.stderr) + self.assertIn("closure", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_empty_closures_and_script_targets_remain_valid(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "graph.json" + for target, closures in ((None, {}), ("unit", {"unit": []})): + with self.subTest(target=target): + graph = {"tests": {"unit": {"target": target, "registered": [], "arguments": [], + "sets": [], "requires": [], "depends": []}}, "closures": closures} + path.write_text(json.dumps(graph), encoding="utf-8") + self.assertEqual(select_tests.load_graph(path), graph) + path.write_text('{"tests": {}, "closures": {}}', encoding="utf-8") + self.assertEqual(select_tests.load_graph(path), {"tests": {}, "closures": {}}) + + if __name__ == "__main__": - unittest.main() + unittest.main(defaultTest=os.environ.get("ANYPS5_SELECT_TEST_CLASS")) From d7744129b2b2a7af8b828146635aec74ee33c4d2 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:17:57 +0800 Subject: [PATCH 250/702] fix(tools): validate trace interval summary fields --- tools/tests/test_trace_digest.py | 59 +++++++++++++++++++++++++++++++- tools/trace_digest.py | 50 ++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 67b93b73c6..2e7eb88420 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -943,7 +943,7 @@ class IntervalDigestTests(unittest.TestCase): def test_bom_encoded_intervals_load(self): with tempfile.TemporaryDirectory() as directory: intervals_file = Path(directory) / "intervals.jsonl" - intervals_file.write_bytes(b"\xef\xbb\xbf" + json.dumps({"interval_start_s": 0.0, "interval_end_s": 10.0, "presents": 600}).encode("utf-8") + b"\n") + intervals_file.write_bytes(b"\xef\xbb\xbf" + json.dumps(interval_line(0.0, 10.0, 20.0, 5.0, presents=600)).encode("utf-8") + b"\n") loaded = trace_digest.load_intervals(intervals_file) self.assertEqual(len(loaded), 1) self.assertEqual(loaded[0]["interval_start_s"], 0.0) @@ -983,5 +983,62 @@ class IntervalDigestTests(unittest.TestCase): self.assertEqual(err.errors, "backslashreplace") +class IntervalSchemaTests(unittest.TestCase): + def setUp(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.path = Path(directory.name) / "summary.jsonl" + + def write(self, record): + self.path.write_text("\n" + json.dumps(SUMMARY) + "\n\n" + json.dumps(record) + "\n", encoding="utf-8") + + def test_invalid_interval_fields_report_physical_line_numbers(self): + baseline = interval_line(0, 1, 20, 1) + changes = [("interval_start_s", value) for value in (-1, True, "0", None, float("nan"), float("inf"), 10**400)] + changes += [("interval_end_s", -1), ("interval_start_s", 2)] + changes += [("presents", value) for value in (-1, True, 1.5, "1", None)] + changes += [("frame_ms", value) for value in (None, [], {"p50": 1})] + changes += [("gpu_busy_ms_per_present", value) for value in (-1, False, "1", float("inf"))] + changes += [("worker_busy_pct", value) for value in ([], None, {"worker": "busy"}, {"worker": -1})] + changes += [("top_phases_ms_per_frame", value) for value in ([], {"phase": True}, {"phase": float("nan")})] + changes += [("trace_dropped", value) for value in (None, 0, "false")] + for field, value in changes: + with self.subTest(field=field, value=value): + self.write({**baseline, field: value}) + with self.assertRaisesRegex(ValueError, r"summary\.jsonl:4:"): + trace_digest.load_intervals(self.path) + + def test_invalid_percentile_values_are_rejected(self): + for field in ("p50", "p90", "p99", "max"): + for value in (True, -1, "bad", float("nan")): + with self.subTest(field=field, value=value): + record = interval_line(0, 1, 20, 1) + record["frame_ms"][field] = value + self.write(record) + with self.assertRaisesRegex(ValueError, r"summary\.jsonl:4:"): + trace_digest.load_intervals(self.path) + + def test_json_and_markdown_reject_bad_intervals_without_tracebacks(self): + self.write(interval_line(0, 1, 20, 1, presents=-1)) + for options in ([], ["--json"]): + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.path), *options], + capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("summary.jsonl:4:", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + + def test_zero_frame_intervals_optional_maps_and_large_worker_percentages_remain_valid(self): + record = interval_line(0, 0, 0, 0, presents=0, frame_ms=dict(p50=0, p90=0, p99=0, max=0), worker_busy_pct={"worker": 125.0}) + record.pop("top_phases_ms_per_frame", None) + record.pop("trace_dropped", None) + self.write(record) + loaded = trace_digest.load_intervals(self.path) + self.assertEqual(loaded, [record]) + digest = trace_digest.digest_intervals(loaded) + self.assertEqual(digest["intervals"][0]["busiest_worker"]["pct"], 125) + self.assertIn("0-0", trace_digest.markdown_intervals(digest)) + + if __name__ == "__main__": unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py index e698b01b3b..edd0e5495f 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -4,6 +4,7 @@ import collections import gzip import html import json +import math import sys import zlib from pathlib import Path @@ -97,7 +98,7 @@ def load_trace(path): return Trace(document) -def load_summary_lines(path): +def load_summary_lines(path, numbered=False): records = [] for number, line in enumerate(Path(path).read_text(encoding="utf-8-sig").split("\n"), 1): if not line.strip(): @@ -108,7 +109,7 @@ def load_summary_lines(path): raise ValueError(f"{path}:{number}: invalid JSON: {error.msg}") from error if not isinstance(record, dict): raise ValueError(f"{path}:{number}: summary record must be an object") - records.append(record) + records.append((number, record) if numbered else record) return records @@ -303,8 +304,49 @@ def markdown(result): def load_intervals(path): - lines = load_summary_lines(path) - return [line for line in lines if "interval_start_s" in line] + lines = [] + for number, record in load_summary_lines(path, numbered=True): + if "interval_start_s" not in record: + continue + try: + validate_interval(record) + except ValueError as error: + raise ValueError(f"{path}:{number}: {error}") from error + lines.append(record) + return lines + + +def interval_number(value, field): + try: + valid = type(value) in (int, float) and math.isfinite(value) and value >= 0 + except OverflowError: + valid = False + if not valid: + raise ValueError(f"{field} must be a finite non-negative number") + return value + + +def validate_interval(record): + start = interval_number(record.get("interval_start_s"), "interval_start_s") + end = interval_number(record.get("interval_end_s"), "interval_end_s") + if end < start: + raise ValueError("interval_end_s must not precede interval_start_s") + if type(record.get("presents")) is not int or record["presents"] < 0: + raise ValueError("presents must be a non-negative integer") + frames = record.get("frame_ms") + if not isinstance(frames, dict): + raise ValueError("frame_ms must be an object") + for field in ("p50", "p90", "p99", "max"): + interval_number(frames.get(field), f"frame_ms.{field}") + interval_number(record.get("gpu_busy_ms_per_present"), "gpu_busy_ms_per_present") + for field in ("worker_busy_pct", "top_phases_ms_per_frame"): + values = record.get(field, {}) + if not isinstance(values, dict): + raise ValueError(f"{field} must be an object") + for name, value in values.items(): + interval_number(value, f"{field}.{name}") + if "trace_dropped" in record and type(record["trace_dropped"]) is not bool: + raise ValueError("trace_dropped must be a boolean") def sparkline(values): From f82d22aacb1051a32a1403de9b4f276146e4ebac Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:17:59 +0800 Subject: [PATCH 251/702] fix(tools): validate web release metadata before publication --- tools/build_web.py | 49 +++++++++++++++++++++++++++++------ tools/tests/test_build_web.py | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/tools/build_web.py b/tools/build_web.py index 4127eaf9a0..4d6692945a 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -33,22 +33,55 @@ def request(url): return urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=60) +def release_metadata(release): + if not isinstance(release, dict): + raise ValueError("release metadata must be a JSON object") + for field in ("tag_name", "html_url"): + if not isinstance(release.get(field), str) or not release[field]: + raise ValueError(f"release metadata {field} must be nonempty text") + assets = release.get("assets") + if not isinstance(assets, list) or any(not isinstance(asset, dict) or not isinstance(asset.get("name"), str) + or not asset["name"] for asset in assets): + raise ValueError("release metadata assets must be a list of named asset objects") + return release + + +def asset_metadata(asset, maximum): + name = asset["name"] + if type(asset.get("size")) is not int or not 0 < asset["size"] <= maximum: + raise ValueError(f"Invalid release asset {name}: archive size must be a positive integer no greater than {maximum}") + if not isinstance(asset.get("browser_download_url"), str) or not asset["browser_download_url"]: + raise ValueError(f"Invalid release asset {name}: browser_download_url must be nonempty text") + if asset.get("digest") is not None and not isinstance(asset["digest"], str): + raise ValueError(f"Invalid release asset {name}: digest must be text or null") + return asset + + +def library_assets(release): + assets = {} + for platform in ("windows", "linux"): + name = f"prx-{platform}-{release['tag_name']}.zip" + matches = [asset for asset in release["assets"] if asset["name"] == name] + if len(matches) != 1: + raise ValueError(f"Missing or ambiguous release asset: {name}") + assets[platform] = asset_metadata(matches[0], 512 * 1024 * 1024) + return assets + + def publish_libraries(output, release=None): if release is None: with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: release = json.load(response) + release = release_metadata(release) tag = release["tag_name"] if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag): raise ValueError("Invalid release tag") manifest = {"tag": tag, "url": release["html_url"], "platforms": {}} + assets = library_assets(release) destination = output / "libraries" destination.mkdir(exist_ok=True) for platform in ("windows", "linux"): - name = f"prx-{platform}-{tag}.zip" - matches = [asset for asset in release["assets"] if asset["name"] == name] - if len(matches) != 1: - raise ValueError(f"Missing or ambiguous release asset: {name}") - asset = matches[0] + asset = assets[platform] temporary = destination / f"{platform}.download" try: digest = hashlib.sha256() @@ -102,6 +135,7 @@ def publish_libraries(output, release=None): def publish_release(output): with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: release = json.load(response) + release = release_metadata(release) tag = release["tag_name"] if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag) or tag.endswith("."): raise ValueError("Invalid release tag") @@ -109,9 +143,8 @@ def publish_release(output): matches = [asset for asset in release["assets"] if asset["name"] == name] if len(matches) != 1: raise ValueError(f"Missing or ambiguous release asset: {name}; publish a release with the web converter") - asset = matches[0] - if not 0 < asset["size"] <= 128 * 1024 * 1024: - raise ValueError("Invalid web release archive size") + asset = asset_metadata(matches[0], 128 * 1024 * 1024) + library_assets(release) with request(asset["browser_download_url"]) as response: data = response.read(asset["size"] + 1) if len(data) != asset["size"]: diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 78cdb95d0e..3c0506b29e 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -233,5 +233,52 @@ class BuildWebCliTests(unittest.TestCase): self.assertEqual(err.errors, "backslashreplace") +class ReleaseMetadataTests(unittest.TestCase): + def test_malformed_api_responses_are_cli_errors_without_site_changes(self): + documents = ({}, [], None, {"tag_name": 1}, {"tag_name": "v1", "html_url": "url", "assets": None}, + {"tag_name": "v1", "html_url": "url", "assets": [None]}, + {"tag_name": "v1", "html_url": "url", "assets": [{"name": 1}]}) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + web = root / "web" + web.mkdir() + preserved = web / "keep.txt" + preserved.write_bytes(b"previous site") + for document in documents: + with self.subTest(document=document): + errors, output = io.StringIO(), io.StringIO() + with patch.object(build_web, "request", return_value=io.BytesIO(json.dumps(document).encode())), contextlib.redirect_stderr(errors), contextlib.redirect_stdout(output): + with self.assertRaises(SystemExit) as raised: + build_web.main(["--build", str(root), "--release"]) + self.assertEqual(raised.exception.code, 2) + self.assertNotIn("Traceback", errors.getvalue()) + self.assertIn("release", errors.getvalue().lower()) + self.assertEqual(output.getvalue(), "") + self.assertEqual(preserved.read_bytes(), b"previous site") + self.assertEqual(list(root.iterdir()), [web]) + self.assertEqual(list(web.iterdir()), [preserved]) + + def test_selected_asset_metadata_is_checked_before_any_download(self): + def asset(platform): + return {"name": f"prx-{platform}-v1.zip", "size": 10, "browser_download_url": "https://example.invalid/asset", "digest": None} + changes = [("size", x) for x in (True, 0, -1, 1.5, "10", 512*1024*1024+1)] + changes += [("browser_download_url", x) for x in (None, "", 1)] + changes += [("digest", x) for x in (False, 1, [])] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + existing = root / "release.json" + existing.write_bytes(b"previous manifest") + for field, value in changes: + with self.subTest(field=field, value=value): + windows, linux = asset("windows"), asset("linux") + linux[field] = value + release = {"tag_name": "v1", "html_url": "https://example.invalid/v1", "assets": [windows, linux]} + with patch.object(build_web, "request", side_effect=AssertionError("download occurred before preflight")): + with self.assertRaisesRegex(ValueError, "release asset"): + build_web.publish_libraries(root, release) + self.assertEqual(existing.read_bytes(), b"previous manifest") + self.assertEqual(list(root.iterdir()), [existing]) + + if __name__ == "__main__": unittest.main() From d2577ad3b08a41824441eb107ea77f367640e79f Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:18:01 +0800 Subject: [PATCH 252/702] fix(tools): accept UTF-8 BOMs in oracle reports --- tools/hw-oracle/compare_results.py | 2 +- tools/hw-oracle/test_compare_results.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tools/hw-oracle/compare_results.py b/tools/hw-oracle/compare_results.py index dbc855f623..f049a8983c 100644 --- a/tools/hw-oracle/compare_results.py +++ b/tools/hw-oracle/compare_results.py @@ -14,7 +14,7 @@ def integer(value, minimum, maximum): def read_report(path): - report = json.loads(path.read_text(encoding="utf-8")) + report = json.loads(path.read_text(encoding="utf-8-sig")) if not isinstance(report, dict): raise ValueError(f"{path}: expected a JSON object") required = {"schema_version", "assembly_target", "rows", *SETTINGS} diff --git a/tools/hw-oracle/test_compare_results.py b/tools/hw-oracle/test_compare_results.py index 453ca40f9f..d33880b06e 100644 --- a/tools/hw-oracle/test_compare_results.py +++ b/tools/hw-oracle/test_compare_results.py @@ -19,6 +19,30 @@ MODES = dict(ieee=0, denorm32=0, denorm16=3, dx10_clamp=1, round32=0, round16=0, class CompareResultsTests(unittest.TestCase): + def test_utf8_bom_and_crlf_keep_identical_and_different_results(self): + for before_encoding, after_encoding in (("utf-8-sig", "utf-8"), ("utf-8", "utf-8-sig"), ("utf-8-sig", "utf-8-sig")): + for different in (False, True): + with self.subTest(before=before_encoding, after=after_encoding, different=different): + after = copy.deepcopy(self.report) + if different: + after["rows"][0]["output"][0] ^= 1 + self.before.write_bytes(json.dumps(self.report, indent=2).replace("\n", "\r\n").encode(before_encoding)) + self.after.write_bytes(json.dumps(after, indent=2).replace("\n", "\r\n").encode(after_encoding)) + result = subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], + capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, int(different), result.stdout + result.stderr) + self.assertEqual(result.stderr, "") + + def test_bom_does_not_hide_invalid_json_or_invalid_reports(self): + self.before.write_text(json.dumps(self.report), encoding="utf-8") + for payload in ("{", "[]", '{"schema_version": 2}'): + with self.subTest(payload=payload): + self.after.write_text(payload, encoding="utf-8-sig") + result = subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], + capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertNotIn("Traceback", result.stderr) + def setUp(self): directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) From 4e982cf80a30a745465cecddad51aa9c8bdb1ec9 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:42:40 +0000 Subject: [PATCH 253/702] feat(libSceAvcap2): declare 7 pending exports --- core/libs/prx/libSceAvcap2/Export.cpp | 37 +++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAvcap2/Export.cpp b/core/libs/prx/libSceAvcap2/Export.cpp index d81f4a5e32..c3423c0278 100644 --- a/core/libs/prx/libSceAvcap2/Export.cpp +++ b/core/libs/prx/libSceAvcap2/Export.cpp @@ -1,10 +1,43 @@ -#include <cstddef> #include <cstdint> +#include <cstddef> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAvcap2Initialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvcap2Terminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvcap2OpenAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvcap2Close(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvcap2Start(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvcap2Stop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvcap2ReadAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } From e90264af44d5bd19bfa960f7c29b10a2625af05f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:42:40 +0000 Subject: [PATCH 254/702] docs(libSceAvcap2): record 7 pending exports --- docs/dev/debt/libSceAvcap2.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/dev/debt/libSceAvcap2.md diff --git a/docs/dev/debt/libSceAvcap2.md b/docs/dev/debt/libSceAvcap2.md new file mode 100644 index 0000000000..7e3b3c63cc --- /dev/null +++ b/docs/dev/debt/libSceAvcap2.md @@ -0,0 +1,13 @@ +# libSceAvcap2 technical debt + +### Unknown function info + +The 7 exports in [Export.cpp](../../../core/libs/prx/libSceAvcap2/Export.cpp) are declared from the NIDs that the PS5 homebrew project [HearBridge-PS5](https://github.com/X-F1REBALL-X/HearBridge-PS5/blob/main/src/avcap2.c) resolves from `libSceAvcap2` (`avcap2.c`, lines 23-29); each NID was checked to equal the hash of the `sceAvcap2*` name given here. Whether these are all the exports of the PS5 module is unknown, and prototypes and behaviour are unverified: every function takes no parameters in the import-resolution convention and throws through `NotImplemented_nid_no_patch`. + +- [sceAvcap2Initialize](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID svzPXluOz8U) +- [sceAvcap2Terminate](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID gojqghDU+1Y) +- [sceAvcap2OpenAudio](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID 3nHpE7Dp5SE) +- [sceAvcap2Close](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID tR7gPe1i8hw) +- [sceAvcap2Start](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID 8CGNCwBsItI) +- [sceAvcap2Stop](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID Z5dKK0xnQ8g) +- [sceAvcap2ReadAudio](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID WhsHggqdwcg) From 7cbf80eabe3b908b361ec042754ed39cf7854322 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 02:20:57 +0000 Subject: [PATCH 255/702] test(agc): cover the DCB and ACB acquire-mem packets, their range rounding and refusals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/tests/Commands.cpp | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index b65ee3c459..fffd65a9a5 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -12,6 +12,8 @@ #include "prx/libSceAgc/DcbState/include/Display.hpp" #include "prx/libSceAgc/DcbState/include/ContextState.hpp" #include "prx/libSceAgc/DcbFlow/include/Sync.hpp" +#include "prx/libSceAgc/DcbFlow/include/Memory.hpp" +#include "prx/libSceAgc/Acb/include/Memory.hpp" #include "prx/libSceAgc/DcbFlow/include/Control.hpp" #include "prx/libSceAgc/DcbDraw/include/Instancing.hpp" #include "prx/libSceAgc/DcbDraw/include/IndexBuffer.hpp" @@ -839,6 +841,30 @@ void testEventWrite() { check(cacheFlushWithAddress.words[0] == 0xc0004600u && cacheFlushWithAddress.words[1] == 0x16u, "incorrect event 0x16 with address packet"); } +void testAcquireMem() { + Storage storage; + const auto* base = reinterpret_cast<const volatile void*>(0x1234567890ull); + const auto* packet = sceAgcDcbAcquireMem(&storage.buffer, 0, 3, 0x1234u, base, 0x1000, 400); + const std::array<std::uint32_t, 8> expected{0xc0065800u, 3, 0x11, 0, 0x12345678u, 0, 10, 0x1234u}; + check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), storage.words.begin()), "acquire packet does not round its range outward to 256 bytes"); + Storage whole; + sceAgcDcbAcquireMem(&whole.buffer, 0, 0, 0, base, 0xffffffffffffffffull, 0); + check(whole.words[0] == 0xc0065800u && whole.words[2] == 0 && whole.words[4] == 0 && whole.words[6] == 0, "an unbounded acquire did not cover the whole address space"); + Storage large; + sceAgcDcbAcquireMem(&large.buffer, 0, 0, 0, base, (1ull << 40u) + 1u, 0); + check(large.words[2] == 0 && large.words[4] == 0, "an acquire over 1 TiB did not cover the whole address space"); + Storage asynchronous; + sceAgcAcbAcquireMem(&asynchronous.buffer, 0x55u, base, 0x100, 80); + const std::array<std::uint32_t, 8> compute{0xc0065800u, 0x80000000u, 2, 0, 0x12345678u, 0, 2, 0x55u}; + check(std::equal(compute.begin(), compute.end(), asynchronous.words.begin()), "the asynchronous compute acquire did not select engine 1 with no CB/DB operation"); + Storage rejected; + expectFailure([&] { sceAgcDcbAcquireMem(&rejected.buffer, 2, 0, 0, base, 0x100, 0); }); + expectFailure([&] { sceAgcDcbAcquireMem(&rejected.buffer, 0, 0, 0x80000u, base, 0x100, 0); }); + expectFailure([&] { sceAgcDcbAcquireMem(&rejected.buffer, 0, 0, 0, base, 0x100, 40u * 0x10000u); }); + check(rejected.words[0] == 0, "a rejected acquire wrote a packet"); + check(sceAgcDcbAcquireMemGetSize() == 32 && sceAgcAcbAcquireMemGetSize() == 32, "acquire packet size"); +} + } int main(int argc, char** argv) { @@ -852,6 +878,7 @@ int main(int argc, char** argv) { } testPackets(); testEventWrite(); + testAcquireMem(); testNop(); testClearState(); testIndexedIndirectDraws(); From 68e56677670895cbd3a9c11813bf8c8a73b178a2 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 02:22:09 +0000 Subject: [PATCH 256/702] refactor(libSceAvcap2): declare the exports in the public header Api.hpp declares each export once, with the placeholder signature its stub has, and the umbrella header and Export.cpp include it. --- core/libs/prx/libSceAvcap2/Export.cpp | 1 + core/libs/prx/libSceAvcap2/include/Api.hpp | 15 +++++++++++++++ core/libs/prx/libSceAvcap2/libSceAvcap2.hpp | 2 ++ 3 files changed, 18 insertions(+) create mode 100644 core/libs/prx/libSceAvcap2/include/Api.hpp diff --git a/core/libs/prx/libSceAvcap2/Export.cpp b/core/libs/prx/libSceAvcap2/Export.cpp index c3423c0278..2afd1f25de 100644 --- a/core/libs/prx/libSceAvcap2/Export.cpp +++ b/core/libs/prx/libSceAvcap2/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAvcap2/include/Api.hpp" #include <cstdint> #include <cstddef> #include "SceTypes.hpp" diff --git a/core/libs/prx/libSceAvcap2/include/Api.hpp b/core/libs/prx/libSceAvcap2/include/Api.hpp new file mode 100644 index 0000000000..66afd355f3 --- /dev/null +++ b/core/libs/prx/libSceAvcap2/include/Api.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceAvcap2Initialize(void); +int APS5_VABI sceAvcap2Terminate(void); +int APS5_VABI sceAvcap2OpenAudio(void); +int APS5_VABI sceAvcap2Close(void); +int APS5_VABI sceAvcap2Start(void); +int APS5_VABI sceAvcap2Stop(void); +int APS5_VABI sceAvcap2ReadAudio(void); + +} diff --git a/core/libs/prx/libSceAvcap2/libSceAvcap2.hpp b/core/libs/prx/libSceAvcap2/libSceAvcap2.hpp index 6f70f09bee..7ca2e6f1de 100644 --- a/core/libs/prx/libSceAvcap2/libSceAvcap2.hpp +++ b/core/libs/prx/libSceAvcap2/libSceAvcap2.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceAvcap2/include/Api.hpp" From 1750e62268af1d969821ee5765e8c9104976ea3e Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 02:22:48 +0000 Subject: [PATCH 257/702] test(agc): cover the graphics and compute dispatch-indirect and direct dispatch packets Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/tests/Commands.cpp | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index b65ee3c459..efe9985798 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -14,6 +14,9 @@ #include "prx/libSceAgc/DcbFlow/include/Sync.hpp" #include "prx/libSceAgc/DcbFlow/include/Control.hpp" #include "prx/libSceAgc/DcbDraw/include/Instancing.hpp" +#include "prx/libSceAgc/Acb/include/Dispatch.hpp" +#include "prx/libSceAgc/Cb/include/Dispatch.hpp" +#include "prx/libSceAgc/DcbFlow/include/Dispatch.hpp" #include "prx/libSceAgc/DcbDraw/include/IndexBuffer.hpp" #include "prx/libSceAgc/DcbDraw/include/DrawIndexed.hpp" #include "prx/libSceAgc/DcbDraw/include/DrawNonIndexed.hpp" @@ -798,6 +801,28 @@ void testDefaultsTypes() { checkCompactTypes(sceAgcGetRegisterDefaults2Internal(0), 15, 0xf58fea31u, 0x00040400u, 0); } +void testDispatchPackets() { + Storage graphics; + const auto* indirect = sceAgcDcbDispatchIndirect(&graphics.buffer, 0x40, 0x8); + check(indirect == graphics.words.data() && graphics.words[0] == 0xc0011600u && graphics.words[1] == 0x40u && graphics.words[2] == 0x49u, "graphics DISPATCH_INDIRECT packet mismatch"); + Storage compute; + const auto* arguments = reinterpret_cast<const volatile void*>(0x123456789abcull); + sceAgcAcbDispatchIndirect(&compute.buffer, arguments, 0x2000u); + const std::array<std::uint32_t, 4> computeExpected{0xc0021600u, 0x56789abcu, 0x1234u, 0x2041u}; + check(std::equal(computeExpected.begin(), computeExpected.end(), compute.words.begin()), "compute DISPATCH_INDIRECT packet mismatch"); + Storage direct; + sceAgcCbDispatch(&direct.buffer, 7, 3, 1, 0x8000u); + const std::array<std::uint32_t, 5> directExpected{0xc0031500u, 7, 3, 1, 0x8041u}; + check(std::equal(directExpected.begin(), directExpected.end(), direct.words.begin()), "DISPATCH_DIRECT packet mismatch"); + Storage rejected; + expectFailure([&] { sceAgcDcbDispatchIndirect(&rejected.buffer, 0, 0x2u); }); + expectFailure([&] { sceAgcCbDispatch(&rejected.buffer, 1, 1, 1, 0x100u); }); + expectFailure([&] { sceAgcAcbDispatchIndirect(&rejected.buffer, reinterpret_cast<const volatile void*>(0x1002ull), 0); }); + expectFailure([&] { sceAgcAcbDispatchIndirect(&rejected.buffer, nullptr, 0); }); + check(rejected.words[0] == 0, "a rejected dispatch wrote a packet"); + check(sceAgcDcbDispatchIndirectGetSize() == 12 && sceAgcAcbDispatchIndirectGetSize() == 16 && sceAgcCbDispatchGetSize() == 20, "dispatch packet sizes"); +} + void testEventWrite() { Storage storage; const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(0x1000)); @@ -853,6 +878,7 @@ int main(int argc, char** argv) { testPackets(); testEventWrite(); testNop(); + testDispatchPackets(); testClearState(); testIndexedIndirectDraws(); testTagHeaders(); From ffe161edde701696f1f331033d84e1e8e82d6883 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 02:41:04 +0000 Subject: [PATCH 258/702] test(libSceAmpr): cover the versioned write-address command, its sizes and the buffer type Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestAmpr.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 7b5bc20439..3af61a4692 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -1277,6 +1277,26 @@ void TestAmmPrt() { Require(sceAmprAmmMeasureAmmCommandSizeAllocatePaForPrt(prt + 8, page, 0, cpuReadWrite) == ammRejected); } +void TestWriteAddressCommand() { + Recorder recorder; + alignas(8) std::uint64_t target = 0; + const auto offset = recorder.Offset(); + const auto commands = recorder.Commands(); + Require(sceAmprCommandBufferWriteAddress_04_00(&recorder.buffer, &target, 0x1122334455667788ull, 0) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::WriteAddress, sceAmprMeasureCommandSizeWriteAddress_04_00()); + Apr::WriteAddressCommand command{}; + std::memcpy(&command, recorder.memory.data() + offset, sizeof(command)); + Require(command.address == reinterpret_cast<std::uint64_t>(&target) && command.value == 0x1122334455667788ull && command.flags == 0); + Require(sceAmprMeasureCommandSizeWriteAddress_04_00() == sizeof(Apr::WriteAddressCommand)); + Require(sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00(0, 0, 0) == sizeof(Apr::WriteKernelEventQueueCommand)); + Require(sceAmprCommandBufferGetType(&recorder.buffer) == static_cast<std::uint32_t>(Apr::BufferType::Apr)); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + Require(target == 0x1122334455667788ull); + Apr::CommandBufferObject generic{}; + Require(sceAmprCommandBufferConstructor(&generic) == 0); + Require(sceAmprCommandBufferGetType(&generic) == static_cast<std::uint32_t>(Apr::BufferType::Generic)); +} + void TestKernelWriteCommands() { std::uint64_t start = 0; std::uint64_t end = 0; @@ -1370,6 +1390,7 @@ int main() { TestFullBuffer(); TestOffsetPastBuffer(); TestSubmission(); + TestWriteAddressCommand(); TestKernelEventQueue(); TestWaits(); TestWaitSatisfiedAfterSubmission(); From 0c02f0be96b775ea38e00f3f24968e4b7310338a Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 02:46:27 +0000 Subject: [PATCH 259/702] test(agc): keep the PARTIAL_TG_EN dispatch form unpinned and check refused packets leave the buffer unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/tests/Commands.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index efe9985798..0c2d794d06 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -815,11 +815,15 @@ void testDispatchPackets() { const std::array<std::uint32_t, 5> directExpected{0xc0031500u, 7, 3, 1, 0x8041u}; check(std::equal(directExpected.begin(), directExpected.end(), direct.words.begin()), "DISPATCH_DIRECT packet mismatch"); Storage rejected; - expectFailure([&] { sceAgcDcbDispatchIndirect(&rejected.buffer, 0, 0x2u); }); + rejected.words.fill(0xa5a5a5a5u); + const auto rejectedWords = rejected.words; + const auto* rejectedCursor = rejected.buffer.cursor_up; expectFailure([&] { sceAgcCbDispatch(&rejected.buffer, 1, 1, 1, 0x100u); }); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); expectFailure([&] { sceAgcAcbDispatchIndirect(&rejected.buffer, reinterpret_cast<const volatile void*>(0x1002ull), 0); }); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); expectFailure([&] { sceAgcAcbDispatchIndirect(&rejected.buffer, nullptr, 0); }); - check(rejected.words[0] == 0, "a rejected dispatch wrote a packet"); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); check(sceAgcDcbDispatchIndirectGetSize() == 12 && sceAgcAcbDispatchIndirectGetSize() == 16 && sceAgcCbDispatchGetSize() == 20, "dispatch packet sizes"); } From ce3fe2f54da1a952d735a7eb5f16c6d80a579fa6 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 02:46:27 +0000 Subject: [PATCH 260/702] test(agc): check that refused packets leave the whole command buffer unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/tests/Commands.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index fffd65a9a5..fc5733644e 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -858,10 +858,15 @@ void testAcquireMem() { const std::array<std::uint32_t, 8> compute{0xc0065800u, 0x80000000u, 2, 0, 0x12345678u, 0, 2, 0x55u}; check(std::equal(compute.begin(), compute.end(), asynchronous.words.begin()), "the asynchronous compute acquire did not select engine 1 with no CB/DB operation"); Storage rejected; + rejected.words.fill(0xa5a5a5a5u); + const auto rejectedWords = rejected.words; + const auto* rejectedCursor = rejected.buffer.cursor_up; expectFailure([&] { sceAgcDcbAcquireMem(&rejected.buffer, 2, 0, 0, base, 0x100, 0); }); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); expectFailure([&] { sceAgcDcbAcquireMem(&rejected.buffer, 0, 0, 0x80000u, base, 0x100, 0); }); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); expectFailure([&] { sceAgcDcbAcquireMem(&rejected.buffer, 0, 0, 0, base, 0x100, 40u * 0x10000u); }); - check(rejected.words[0] == 0, "a rejected acquire wrote a packet"); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); check(sceAgcDcbAcquireMemGetSize() == 32 && sceAgcAcbAcquireMemGetSize() == 32, "acquire packet size"); } From a5a8451218a71e52fe29d741ba4b8eea2329b9e0 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 02:46:59 +0000 Subject: [PATCH 261/702] test(libkernel): cover sceKernelBatchMap mapping and unmapping at the given starts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestMemory.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index df4e2041c9..ec0a4cacf0 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -404,6 +404,27 @@ static void CheckBatchMapStopsAtInvalidEntry() { Require(sceKernelMunmap(reinterpret_cast<std::uint64_t>(entries[0].start), page) == 0); } +static void CheckBatchMapPlacesAtTheGivenStart() { + constexpr std::size_t page = 0x4000; + constexpr int mapFlexible = 3; + constexpr int unmap = 1; + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0, page) == 0 && reserved != nullptr); + auto* base = static_cast<unsigned char*>(reserved); + KernelBatchMapEntry entries[2] = {{base, 0, page, 3, 0, 0, mapFlexible}, {base + page, 0, page, 3, 0, 0, mapFlexible}}; + int processed = -1; + Require(sceKernelBatchMap(entries, 2, &processed) == 0 && processed == 2); + Require(entries[0].start == base && entries[1].start == base + page); + base[0] = 0x5a; + base[page * 2 - 1] = 0xa5; + Require(base[0] == 0x5a && base[page * 2 - 1] == 0xa5); + KernelBatchMapEntry unmaps[2] = {entries[0], entries[1]}; + unmaps[0].operation = unmap; + unmaps[1].operation = unmap; + processed = -1; + Require(sceKernelBatchMap(unmaps, 2, &processed) == 0 && processed == 2); +} + static void CheckVirtualQueryFindsNeighbours() { constexpr std::size_t page = 0x4000; void* low = nullptr; @@ -3333,6 +3354,7 @@ int main(int argc, char** argv) { CheckMunmapScopedFlexibleRelease(); CheckGuestModuleUsesFlexibleMemory(); CheckBatchMapStopsAtInvalidEntry(); + CheckBatchMapPlacesAtTheGivenStart(); CheckCheckedReleaseDirectMemory(); CheckReleaseSkipsFreePages(); CheckAudioCoprocessorProtection(); From 2440eab0623901e11fbdaafb18f6b2c2483269b7 Mon Sep 17 00:00:00 2001 From: hugodearaujo <hugoaraujocamara@gmail.com> Date: Mon, 26 Oct 2026 19:04:29 -0300 Subject: [PATCH 262/702] test(agc): read a 4x 10_11_11 float target through a format 36 descriptor --- .../execution/SampleMultisampleTarget.cpp | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 5870d759ac..f2a46408b4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -69,6 +69,8 @@ constexpr std::uint32_t Uint8x4Info = 0x8428u; constexpr std::uint32_t Format8888Unorm = 56; constexpr std::uint32_t Format8888Uint = 60; constexpr std::uint32_t Format8888Sint = 61; +constexpr std::uint32_t Float10_11_11Info = 0x0718u; +constexpr std::uint32_t Format11_11_10Float = 36; constexpr std::uint32_t Type2DMsaa = 14; constexpr std::uint32_t TileRenderTarget = 0x1b; constexpr std::uint32_t TileZ64KBX = 0x18; @@ -230,6 +232,20 @@ AgcDriver::Graphics::ColorTarget DecodeSwappedTarget(const Block& block) { return color; } +AgcDriver::Graphics::ColorTarget DecodePackedFloatTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float10_11_11Info; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the 10_11_11 float target did not decode as a 16x16 B10G11R11 surface of 4 samples"); + return color; +} + AgcDriver::Graphics::ColorTarget DecodeUintTarget(const Block& block) { AgcDriver::Registers cx; cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u); @@ -264,6 +280,20 @@ void CheckSwappedSamples(const std::vector<std::array<float, 4>>& loaded) { Require(drawn != 0, "no sample of the BGRA8 target holds the drawn color"); } +void CheckPackedFloatSamples(const std::vector<std::array<float, 4>>& loaded) { + const std::array<float, 4> expected{Drawn[0], Drawn[1], Drawn[2], 1.0f}; + const std::array<float, 4> background{0.0f, 0.0f, 0.0f, 1.0f}; + std::size_t drawn = 0; + for (const auto& texel : loaded) { + if (texel == background) continue; + char text[256]; + std::snprintf(text, sizeof(text), "a sample of the 10_11_11 float target read through a format 36 descriptor reads (%g, %g, %g, %g) instead of the drawn (%g, %g, %g, 1)", texel[0], texel[1], texel[2], texel[3], expected[0], expected[1], expected[2]); + Require(texel == expected, text); + ++drawn; + } + Require(drawn != 0, "no sample of the 10_11_11 float target holds the drawn color"); +} + void CheckUintSamples(const std::vector<std::array<float, 4>>& loaded) { std::size_t drawn = 0; for (const auto& texel : loaded) { @@ -988,6 +1018,12 @@ int main(int argc, char** argv) { CheckSwappedSamples(swappedSamples); CheckFirstSampleReads(*device, TargetDescriptor(swapped, SampleShift, Format8888Unorm), swappedSamples, true, "from a 4x BGRA8 target read through an RGBA8 descriptor"); std::puts("a 4x BGRA8 color target loaded through an RGBA8 texture descriptor reads its drawn color with red and blue swapped"); + Block packedFloat(4u * BlockBytes); + Draw(*device, TargetState(DecodePackedFloatTarget(packedFloat))); + const auto packedSamples = LoadSamples(*device, packedFloat, Format11_11_10Float); + CheckPackedFloatSamples(packedSamples); + CheckFirstSampleReads(*device, TargetDescriptor(packedFloat, SampleShift, Format11_11_10Float), packedSamples, true, "from a 4x 10_11_11 float target read through a format 36 descriptor"); + std::puts("a 4x 10_11_11 float color target loaded through a format 36 (11_11_10 float) texture descriptor reads its drawn color"); Block integer(4u * BlockBytes); Block signedFill(BlockBytes); if ((AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).limits.sampledImageIntegerSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { From f4274307ecd56b995e43983780601bf6f70f2670 Mon Sep 17 00:00:00 2001 From: hugodearaujo <hugoaraujocamara@gmail.com> Date: Mon, 26 Oct 2026 19:04:41 -0300 Subject: [PATCH 263/702] fix(agc): sample a multisampled 11_11_10 float texture in its sampled format Since #8898 ResolveTextureFormat gives format 36 (11_11_10 float) its storage form R32_UINT, while a sampled format 36 image is read through its native B10G11R11_UFLOAT view (SampledTextureFormat). MultisampleTargetTexture and MultisampleTargetFirstSample still matched the resident target by ResolveTextureFormat, so a 10_11_11 float color target (B10G11R11_UFLOAT) sampled as a format 36 texture of the same shape was refused. --- .../libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 7cb1eafdad..570f31c944 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1244,12 +1244,16 @@ bool sameStorageForm(const Context& context, VkFormat rendered, VkFormat request return storage != VK_FORMAT_UNDEFINED && storage == HostStorageFormat(context, requested); } +VkFormat sampledFormat(const Context& context, std::uint32_t guestFormat) { + return IsRepackedTextureFormat(guestFormat) ? ResolveTextureFormat(guestFormat) : SampledTextureFormat(context, guestFormat); +} + } std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) { std::lock_guard lock(targetsMutex()); discardRetiredTargets(); - const auto format = ResolveTextureFormat(resource.format); + const auto format = sampledFormat(context, resource.format); const MultisampleTarget* other = nullptr; for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; @@ -1272,7 +1276,7 @@ std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const std::shared_ptr<Texture> MultisampleTargetFirstSample(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { std::lock_guard lock(targetsMutex()); discardRetiredTargets(); - const auto format = ResolveTextureFormat(resource.format); + const auto format = sampledFormat(context, resource.format); const MultisampleTarget* other = nullptr; for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; From 8b323065a9e535076c6c5466bd518bb56268e1d6 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 15:59:30 +1300 Subject: [PATCH 264/702] docs(agc): note that only viewport 0 bounds clamp an exported depth The clamp reads PA_SC_VPORT_ZMIN_0 and PA_SC_VPORT_ZMAX_0 only, so a draw that selects another viewport is clamped to viewport 0's depth range. --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 54e7bac478..7badd6ba18 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -244,6 +244,7 @@ - [Multisample image upload](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp) does not model guest layouts, native ownership or CMASK/FMASK/DCC publication; console equivalence and large-title initialization costs remain unmeasured. It stores 1/2/4/8/16-byte elements from a storage buffer into an unsigned 2D multisample array view in layer/y/x/fragment order. It requires enabled multisample storage images and formatless image writes, 2/4/8 fragments and valid aligned buffer and dispatch ranges. The caller supplies the matching storage view in GENERAL and owns barriers, completion and resource lifetimes. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. - [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. The mode is a pipeline specialization constant (`PipelineSpecialization::DepthRemapBase`); scale and offset are read from the draw's push block (two words at byte 104, `DepthRemapPushOffsetBytes`, so a remapped draw's stage push data ends before it), so draws that differ only in them share one vertex module and one pipeline. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. +- [Exported depth clamp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) - only the depth range of viewport 0 (`PA_SC_VPORT_ZMIN_0` and `PA_SC_VPORT_ZMAX_0`) is read, so a pixel shader that exports depth in a draw that selects another viewport is clamped to viewport 0's range, not to its own. `DB_RENDER_OVERRIDE.DISABLE_VIEWPORT_CLAMP` still stores the exported value as it is. - [Benchmark rewrite failure test off Windows](../../../core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp) (libSceAgcDriver) - `ReplaceGzipFile` checks that a write failing at `<name>.tmp` keeps the previous `aps5-bench.json.gz` only on non-Windows builds: the `std::ios_base::failure` thrown inside the MinGW ofstream did not unwind to the test's catch, so Windows CI covers only the failed move. - [Sampled array windows](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver) - a sampled 2D array, 1D array or cube view whose base array slice is past 0 is built from its viewed slices only (`ViewedArrayWindow`), at the guest address of its base slice. The window is taken only when the surface is thin, has no DCC metadata, and, for a 64 KiB XOR swizzle, when the base slice keeps every slice bit of the swizzle equation at zero (R_X 1-byte elements: a base slice that is a multiple of 16). Any other view still builds slices 0 to its last slice. A window and a whole-array view of the same surface are separate images over overlapping memory, kept coherent by the existing overlap rules, at the cost of the memory and uploads of both. PPSA26344 samples a 256x256 cube array at slices 4299..7232, built at 1467 MiB instead of about 3.8 GB. From 319f275322cc81a6a89617d32c20d8979601b7ae Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 03:02:39 +0000 Subject: [PATCH 265/702] test(agc): cover the DMA data patch setters' address words and refusals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/CMakeLists.txt | 1 + core/libs/prx/libSceAgc/tests/DmaData.cpp | 36 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 82331aea05..5d52a70f80 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -395,6 +395,7 @@ add_test_executable(agc_dma_data_tests EXCLUDE_FROM_ALL Command/src/Memory.cpp Acb/src/Memory.cpp DcbFlow/src/Memory.cpp + Patch/src/Dma.cpp ) target_include_directories(agc_dma_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_dma_data_tests PRIVATE libSceAgcDriver libc) diff --git a/core/libs/prx/libSceAgc/tests/DmaData.cpp b/core/libs/prx/libSceAgc/tests/DmaData.cpp index 880035d394..602b7cd7d3 100644 --- a/core/libs/prx/libSceAgc/tests/DmaData.cpp +++ b/core/libs/prx/libSceAgc/tests/DmaData.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgc/DcbFlow/include/Memory.hpp" #include "prx/libSceAgc/Acb/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" +#include "prx/libSceAgc/Patch/include/Dma.hpp" #include <algorithm> #include <array> @@ -73,6 +74,40 @@ void testCommand(TWriter writer, TSize size, std::uint32_t control) { check(shortBuffer.words == before && shortBuffer.buffer.cursor_up == shortBuffer.words.data(), "failed DMA write modified the buffer"); } +void testPatches() { + constexpr std::uint64_t PatchedDestination = 0x0000aabbccddeeffull; + constexpr std::uint64_t PatchedSource = 0x0000102030405060ull; + for (auto* writer : {writeCompute, writeDraw}) { + Storage storage; + auto* packet = writer(&storage.buffer); + auto expected = storage.words; + check(sceAgcDmaDataPatchSetDstAddressOrOffset(packet, PatchedDestination) == 0, "the destination patch failed"); + expected[4] = 0xccddeeffu; + expected[5] = 0xaabbu; + check(storage.words == expected, "the destination patch did not replace only words 4 and 5"); + check(sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(packet, PatchedSource) == 0, "the source patch failed"); + expected[2] = 0x30405060u; + expected[3] = 0x1020u; + check(storage.words == expected, "the source patch did not replace only words 2 and 3"); + check(sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(packet, 0xfedcba9876543210ull) == 0 && packet[2] == 0x76543210u && packet[3] == 0xfedcba98u, "the source patch did not keep a 64-bit immediate whole"); + } + + Storage refused; + auto* packet = writeDraw(&refused.buffer); + packet[0] = 0xc0051000u; + auto before = refused.words; + expectFailure([&] { sceAgcDmaDataPatchSetDstAddressOrOffset(packet, PatchedDestination); }); + expectFailure([&] { sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(packet, PatchedSource); }); + check(refused.words == before, "a patch of a NOP changed it"); + packet[0] = 0xc0045000u; + before = refused.words; + expectFailure([&] { sceAgcDmaDataPatchSetDstAddressOrOffset(packet, PatchedDestination); }); + expectFailure([&] { sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(packet, PatchedSource); }); + check(refused.words == before, "a patch of a short DMA packet changed it"); + expectFailure([] { sceAgcDmaDataPatchSetDstAddressOrOffset(nullptr, PatchedDestination); }); + expectFailure([] { sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(nullptr, PatchedSource); }); +} + } int main() { @@ -80,6 +115,7 @@ int main() { testCommand(writeCompute, sceAgcAcbDmaDataGetSize, 0x44102000u); testCommand(writeDraw, sceAgcDcbDmaDataGetSize, 0xc4102001u); check(sceAgcAcbDmaDataGetSize() == sceAgcDcbDmaDataGetSize(), "compute and draw DMA sizes differ"); + testPatches(); std::puts("AGC DMA data tests passed"); return 0; } catch (const std::exception& error) { From 72801be9faed2c13aa163251c3e5c05286471783 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 03:10:56 +0000 Subject: [PATCH 266/702] test(libSceAmpr): check the measured kernel event queue command size against the recorded one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestAmpr.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 7b5bc20439..d151d99119 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -224,6 +224,7 @@ void TestKernelEventQueue() { Require(sceKernelAddAmprEvent(eq, 7, &userData) == 0); Recorder recorder; Require(sceAmprCommandBufferWriteKernelEventQueue_04_00(&recorder.buffer, static_cast<std::uint64_t>(eq), 7, 0x1234, 0) == 0); + RequireAppended(recorder, 0, 0, Apr::Opcode::WriteKernelEventQueue, sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00(static_cast<std::uint64_t>(eq), 7, 0x1234)); Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); KernelEvent event{}; int count = 0; From 90b10d8b547870c33593739709f6525b84ec455a Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 21:25:37 -0500 Subject: [PATCH 267/702] test(libSceNet): cover null length pointer and invalid descriptor in socket option calls --- core/libs/prx/libSceNet/tests/Ipv6Only.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceNet/tests/Ipv6Only.cpp b/core/libs/prx/libSceNet/tests/Ipv6Only.cpp index 9470a5be35..b34ae99196 100644 --- a/core/libs/prx/libSceNet/tests/Ipv6Only.cpp +++ b/core/libs/prx/libSceNet/tests/Ipv6Only.cpp @@ -48,6 +48,9 @@ static void Exercise(int descriptor) { RequireOption(descriptor, 1); std::uint32_t length = sizeof(enabled); int output = 0; + Require(sceNetGetsockopt(descriptor, 41, 27, &output, nullptr) == static_cast<int>(0x80410116u) && *sceNetErrnoLoc() == 22); + Require(sceNetGetsockopt(-1, 41, 27, &output, &length) == static_cast<int>(0x80410109u) && *sceNetErrnoLoc() == 9); + Require(sceNetSetsockopt(-1, 41, 27, &enabled, sizeof(enabled)) == static_cast<int>(0x80410109u) && *sceNetErrnoLoc() == 9); Require(sceNetGetsockopt(descriptor, 41, 28, &output, &length) == static_cast<int>(0x8041012au) && *sceNetErrnoLoc() == 42); Require(sceNetSetsockopt(descriptor, 41, 28, &enabled, sizeof(enabled)) == static_cast<int>(0x8041012au) && *sceNetErrnoLoc() == 42); } From 2f52282590d9678597e18414e39c3ff82a7a647e Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:17:59 +0800 Subject: [PATCH 268/702] fix(ci): reject tests that were not run before skip exemptions --- tools/check_skips.py | 4 ++-- tools/tests/test_check_skips.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index 936570e00e..394cf76d0c 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -39,8 +39,8 @@ def skips(path, require_tests=False): found = False for case in root.iter("testcase"): found = True - if case.get("status") == "disabled": - raise ValueError(f"{path}: testcase {case.get('name')!r} is disabled and was not run") + if case.get("status") in ("disabled", "notrun"): + raise ValueError(f"{path}: testcase {case.get('name')!r} is {case.get('status')} and was not run") skipped = case.find("skipped") if skipped is None: continue diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 567a04612c..8670994612 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -436,5 +436,25 @@ class SkipReportTests(unittest.TestCase): self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) +class NotRunTests(unittest.TestCase): + def test_real_notrun_case_is_rejected_before_windows_agc_exemption(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "CTestTestfile.cmake").write_text('add_test(agc_missing "' + (root / "missing-program").as_posix() + '")\n', encoding="utf-8") + report = root / "report.xml" + generated = subprocess.run(["ctest", "--test-dir", str(root), "--output-junit", str(report)], + capture_output=True, text=True, timeout=10) + self.assertNotEqual(generated.returncode, 0) + self.assertIn('status="notrun"', report.read_text(encoding="utf-8")) + for job in ("windows", "linux"): + with self.subTest(job=job): + checked = subprocess.run([sys.executable, str(TOOL), str(report), "--job", job, "--require-tests"], + capture_output=True, text=True, timeout=10) + self.assertEqual(checked.returncode, 2, checked.stdout + checked.stderr) + self.assertIn("notrun", checked.stderr) + self.assertIn("agc_missing", checked.stderr) + self.assertNotIn("Traceback", checked.stderr) + + if __name__ == "__main__": unittest.main() From 9c5397dc27b486f5e38638e39b96f94485a0c714 Mon Sep 17 00:00:00 2001 From: Filip Michalski <filip.michalski92@gmail.com> Date: Tue, 27 Oct 2026 04:31:07 +0100 Subject: [PATCH 269/702] perf(shader): reuse starting pixel lane predicates --- .../libSceAgcDriver/tests/ConstantBitTest.cpp | 20 ++++--- .../execution/PixelExecInterpolation.cpp | 6 +- .../Optimization/src/ConstantFolder.cpp | 57 +++++++++++++------ 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp index d7bac76983..5e2fc66ef1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -210,6 +210,8 @@ enum class QuadMask { Nested, SeparateHalves, RealOnly, + RealPartial, + RealDivergent, Partial, WrongHalf, Divergent, @@ -225,7 +227,7 @@ bool quadProjection(std::uint32_t wave, QuadMask shape, bool expected, IrShaderS IrBuilder builder(program); builder.SetInsertionPoint(block); auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); - if (shape == QuadMask::Divergent) { + if (shape == QuadMask::Divergent || shape == QuadMask::RealDivergent) { auto& taken = program.CreateBlock(); auto& other = program.CreateBlock(); program.BlockOrder().push_back(&taken); @@ -245,17 +247,18 @@ bool quadProjection(std::uint32_t wave, QuadMask shape, bool expected, IrShaderS return std::array<IrValue*, 2>{&builder.CompositeExtract(mask, 0u), &builder.CompositeExtract(mask, 1u)}; }; auto& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast<std::uint32_t>(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); - const auto real = ballot(builder.IEqual(helper, builder.Constant(0u))); + auto& realCondition = builder.IEqual(helper, builder.Constant(0u)); + const auto real = ballot(realCondition); const auto quads = expand(real); const auto host = expand(ballot(builder.ConstantBool(true))); auto words = quads; - if (shape == QuadMask::Intersected || shape == QuadMask::Nested || shape == QuadMask::SeparateHalves || shape == QuadMask::RealOnly) { + if (shape == QuadMask::Intersected || shape == QuadMask::Nested || shape == QuadMask::SeparateHalves || shape == QuadMask::RealOnly || shape == QuadMask::RealPartial || shape == QuadMask::RealDivergent) { const auto highHost = shape == QuadMask::SeparateHalves ? expand(ballot(builder.ConstantBool(true))) : host; - const auto source = shape == QuadMask::Nested || shape == QuadMask::RealOnly ? real : quads; + const auto source = shape == QuadMask::Nested || shape == QuadMask::RealOnly || shape == QuadMask::RealPartial || shape == QuadMask::RealDivergent ? real : quads; words = {&builder.BitwiseAnd(*source[0], *host[0]), &builder.BitwiseAnd(*source[1], *highHost[1])}; if (shape == QuadMask::Nested) words = expand(words); } - if (shape == QuadMask::Partial) { + if (shape == QuadMask::Partial || shape == QuadMask::RealPartial) { words = {&builder.BitwiseAnd(*words[0], builder.Constant(0x0000ffffu)), &builder.Constant(0u)}; } if (shape == QuadMask::WrongHalf) words[1] = words[0]; @@ -266,7 +269,8 @@ bool quadProjection(std::uint32_t wave, QuadMask shape, bool expected, IrShaderS static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {})); ConstantFolder{}.Fold(program); const auto* result = user.Argument(0)->Resolve(); - return expected ? result->HasImmediate() && result->ImmediateBool() : !result->HasImmediate(); + if (shape == QuadMask::RealOnly) return result == &realCondition; + return expected ? result->HasImmediate() && result->ImmediateBool() : result->Opcode() == IrOpcode::INotEqual32; } bool run(const char* name, bool passed) { @@ -284,8 +288,10 @@ int main() { passed &= run("intersecting whole-quad entry masks preserves pixel helpers", quadProjection(wave, QuadMask::Intersected, true)); passed &= run("a nested whole-quad entry mask covers pixel helpers", quadProjection(wave, QuadMask::Nested, true)); passed &= run("independent low and high whole-quad masks cover pixel helpers", quadProjection(wave, QuadMask::SeparateHalves, true)); - passed &= run("an unexpanded entry mask must keep the helper bit test", quadProjection(wave, QuadMask::RealOnly, false)); + passed &= run("an unexpanded entry mask must reuse the original helper test", quadProjection(wave, QuadMask::RealOnly, false)); passed &= run("a partial whole-quad mask must keep the lane bit test", quadProjection(wave, QuadMask::Partial, false)); + passed &= run("a partial real-lane mask must keep the lane bit test", quadProjection(wave, QuadMask::RealPartial, false)); + passed &= run("a divergent real-lane ballot must keep the lane bit test", quadProjection(wave, QuadMask::RealDivergent, false)); passed &= run("a divergent whole-quad ballot must keep the lane bit test", quadProjection(wave, QuadMask::Divergent, false)); passed &= run("whole-quad entry proofs do not apply to other stages", quadProjection(wave, QuadMask::Intersected, false, IrShaderStage::Vertex)); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelExecInterpolation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelExecInterpolation.cpp index f9d09c28f4..edd0f8e67b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelExecInterpolation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelExecInterpolation.cpp @@ -28,6 +28,10 @@ alignas(256) constexpr std::array<std::uint32_t, 8> ExecCopyPixelCode{ 0xbefe0a7e, 0xc8080000, 0xbe80047e, 0xbefe0400, 0xc8090001, 0xf800180f, 0x02020202, 0xbf810000, }; +alignas(256) constexpr std::array<std::uint32_t, 8> RestoredEntryExecPixelCode{ + 0xbe80047e, 0xbefe0a7e, 0xc8080000, 0xc8090001, 0xbefe0400, 0xf800180f, 0x02020202, 0xbf810000, +}; + alignas(256) constexpr std::array<std::uint32_t, 7> RepeatedWqmPixelCode{ 0xbefe0a7e, 0xc8080000, 0xbefe0a7e, 0xc8090001, 0xf800180f, 0x02020202, 0xbf810000, }; @@ -131,7 +135,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; - for (const auto code : {std::span<const std::uint32_t>(ExecCopyPixelCode), std::span<const std::uint32_t>(RepeatedWqmPixelCode)}) { + for (const auto code : {std::span<const std::uint32_t>(ExecCopyPixelCode), std::span<const std::uint32_t>(RepeatedWqmPixelCode), std::span<const std::uint32_t>(RestoredEntryExecPixelCode)}) { const auto result = compile(code); Require(!result.barycentricEmulation.active, "changing EXEC between v_interp_p1/p2 needed a generated stage"); Pixels.fill(-7.0f); diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index a7419f9c3a..43db702ad8 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -245,10 +245,24 @@ bool runsAllStartingLanes(const IrProgram& program, const IrBlock* target) { return false; } +bool isRealLaneCondition(const IrValue& condition) { + 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<StageInputKind>(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)); +} + enum class LaneCoverage { Real, Quads }; +constexpr std::uint32_t StartingLaneDepth = 8u; + bool holdsStartingLanes(const IrProgram& program, const IrValue& word, std::uint32_t index, LaneCoverage coverage, std::uint32_t depth = 0) { - if (depth > 8u) return false; + if (depth > StartingLaneDepth) return false; if (word.Opcode() == IrOpcode::BitwiseAnd32) { return holdsStartingLanes(program, resolveArg(word, 0), index, coverage, depth + 1u) && holdsStartingLanes(program, resolveArg(word, 1), index, coverage, depth + 1u); } @@ -266,19 +280,26 @@ bool holdsStartingLanes(const IrProgram& program, const IrValue& word, std::uint 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<StageInputKind>(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)); + return isImmediate(condition, IrType::U1) ? condition.ImmediateBool() : isRealLaneCondition(condition); } -bool foldStartingQuadBitTest(const IrProgram& program, IrBuilder& builder, IrValue& inst) { +IrValue* startingRealLaneCondition(const IrProgram& program, const IrValue& word, std::uint32_t index, std::uint32_t depth = 0u) { + if (depth > StartingLaneDepth) return nullptr; + if (word.Opcode() == IrOpcode::BitwiseAnd32) { + for (std::size_t side = 0u; side < 2u; ++side) { + auto* condition = startingRealLaneCondition(program, resolveArg(word, side), index, depth + 1u); + if (condition != nullptr && holdsStartingLanes(program, resolveArg(word, 1u - side), index, LaneCoverage::Real, depth + 1u)) return condition; + } + return nullptr; + } + if (word.Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediateU32(resolveArg(word, 1), index)) return nullptr; + const auto& ballot = resolveArg(word, 0); + if (ballot.Opcode() != IrOpcode::Ballot || !runsAllStartingLanes(program, ballot.Parent())) return nullptr; + auto& condition = resolveArg(ballot, 0); + return isRealLaneCondition(condition) ? &condition : nullptr; +} + +bool foldStartingLaneBitTest(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); @@ -300,9 +321,13 @@ bool foldStartingQuadBitTest(const IrProgram& program, IrBuilder& builder, IrVal } else if (program.WaveSize() != 32u) { return false; } - if (!holdsStartingLanes(program, *low, 0u, LaneCoverage::Quads)) return false; - if (high != nullptr && !holdsStartingLanes(program, *high, 1u, LaneCoverage::Quads)) return false; - replaceWith(inst, builder.ConstantBool(true)); + if (holdsStartingLanes(program, *low, 0u, LaneCoverage::Quads) && (high == nullptr || holdsStartingLanes(program, *high, 1u, LaneCoverage::Quads))) { + replaceWith(inst, builder.ConstantBool(true)); + return true; + } + auto* condition = startingRealLaneCondition(program, *low, 0u); + if (condition == nullptr || (high != nullptr && startingRealLaneCondition(program, *high, 1u) != condition)) return false; + replaceWith(inst, *condition); return true; } @@ -1225,7 +1250,7 @@ bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { 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; }) || foldEqualityByValueSet(builder, value, true, facts); - 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; }) || foldEqualityByValueSet(builder, value, false, facts); + case IrOpcode::INotEqual32: return foldConstantBitTest(builder, value) || foldStartingLaneBitTest(program, builder, value) || foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a != b; }) || foldEqualityByValueSet(builder, value, false, facts); case IrOpcode::ULessThan32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a < b; }) || foldULessThanByRange(builder, value, facts); 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 36277c4b445da812668508edf41d52a907e51fda Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Sun, 25 Oct 2026 20:51:01 -0500 Subject: [PATCH 270/702] perf(agc): keep recently sampled depth plane copies up to twice the budget The depth plane copy budget (APS5_DEPTH_PLANE_COPY_MIB, 256 MiB) evicted the least recently used copy even when it had been sampled in the same frame. When one frame samples more than the budget, every use evicts a copy the next use needs, so each copy is made and refilled again on every use. ASTRO BOT (PPSA21564) samples a 1536x1536x16 D32 copy (288 MiB with its staging buffer) and a 2048x2048x3 D16 copy (48 MiB) alternately in its intro and thrashes this way. A copy sampled in the current or the previous present now stays while all copies hold at most twice the budget. Past that, the least recently used copies leave whenever they were sampled, so the copies never hold more than twice the budget, or the one copy being sampled if it is larger. Older copies still leave first by least recent use down to the budget. DepthPlaneCopyBytes reports the bytes the copies hold, for the tests. The pooled-memory rounds of agc_driver_depth_plane_guest_layers now advance two presents between samples, since two copies sampled in one present may now be kept together. --- .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/src/DepthSurface.cpp | 13 ++++- .../tests/execution/DepthPlaneGuestLayers.cpp | 4 +- .../tests/execution/DepthPlaneSampling.cpp | 55 +++++++++++++++++++ docs/dev/ENVIRONMENT.md | 2 +- 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index ef9852b1e0..1f5c490121 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -73,6 +73,7 @@ std::uint64_t HtileMetadataBytes(VkExtent2D extent); bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t address, std::size_t bytes); std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D); std::size_t DepthPlaneCopyCount(); +VkDeviceSize DepthPlaneCopyBytes(); VkFormat HostDepthFormat(const Context& context, VkFormat format); float HostDepthBiasConstant(const Context& context, VkFormat format, float constant); std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index b8084357b1..ccb980022e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1282,6 +1282,7 @@ public: const std::uint32_t layers; VkDeviceSize imageBytes = 0; std::uint64_t lastUse = 0; + std::uint64_t lastPresent = 0; VkDeviceSize HeldBytes() const { return imageBytes + (staging != nullptr ? staging->Size() : 0u); } private: @@ -1407,10 +1408,13 @@ void evictPlaneCopies(const std::tuple<VkDevice, std::uint64_t, std::uint32_t, s auto& copies = planeCopies(); VkDeviceSize total = 0; for (const auto& entry : copies) total += entry.second->HeldBytes(); + const auto present = Recorder::Presents(); + const auto ceiling = budget + std::min(budget, std::numeric_limits<VkDeviceSize>::max() - budget); while (total > budget) { + const bool over = total > ceiling; auto oldest = copies.end(); for (auto it = copies.begin(); it != copies.end(); ++it) { - if (it->first == keep) continue; + if (it->first == keep || (!over && it->second->lastPresent + 1u >= present)) continue; if (oldest == copies.end() || it->second->lastUse < oldest->second->lastUse) oldest = it; } if (oldest == copies.end()) break; @@ -2349,6 +2353,12 @@ std::size_t DepthPlaneCopyCount() { return planeCopies().size(); } +VkDeviceSize DepthPlaneCopyBytes() { + VkDeviceSize total = 0; + for (const auto& entry : planeCopies()) total += entry.second->HeldBytes(); + return total; +} + namespace { thread_local bool depthCopySourceActive = false; @@ -2426,6 +2436,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c auto& copy = copies[copyKey]; if (copy == nullptr || copy->layers < layers) copy = std::make_unique<DepthPlaneCopy>(context, base.extent, imageFormat, layers); copy->lastUse = ++planeCopyClock(); + copy->lastPresent = Recorder::Presents(); auto texture = copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); evictPlaneCopies(copyKey); return texture; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index 09383ccc9a..a7c6dde0eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -459,10 +459,12 @@ void PooledPlaneCopies(const Context& context, const WatchedBlock& first, const DepthSurfaceView(context, DepthTarget{first.Address(), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); DepthSurfaceView(context, DepthTarget{second.Address(), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); const auto sample = [&](const WatchedBlock& block, int round) { + Recorder::CountPresent(); + Recorder::CountPresent(); lastFilledImage = VK_NULL_HANDLE; bufferToImageCopies = 0; Require(DepthSurfaceTexture(context, words, ArrayOver(block.Address()), components) != nullptr, "the 2D array over a depth surface was not sampled through its depth planes in round " + std::to_string(round)); - Require(DepthPlaneCopyCount() == 1, "the depth plane copies outgrew a budget that holds one in round " + std::to_string(round) + ", " + std::to_string(DepthPlaneCopyCount()) + " copies"); + Require(DepthPlaneCopyCount() == 1, "the depth plane copies outgrew a budget that holds one, two presents after the other was sampled, in round " + std::to_string(round) + ", " + std::to_string(DepthPlaneCopyCount()) + " copies"); Require(lastFilledImage.load() != VK_NULL_HANDLE && bufferToImageCopies.load() == 1, "a plane copy made again over a full budget was not filled from its surface once in round " + std::to_string(round) + ", " + std::to_string(bufferToImageCopies.load()) + " fills"); }; sample(first, 0); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index f77432e092..108954d47f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -70,6 +71,7 @@ constexpr std::size_t PaddedCubeOffset = 0x120000; constexpr std::size_t SmallCubeOffset = 0x180000; constexpr std::size_t LargeCubeOffset = 0x200000; constexpr std::size_t GuestDepthBitsOffset = 0x480000; +constexpr std::size_t FrameCopiesOffset = 0x520000; constexpr float DrawnDepth = 0.25f; constexpr float DrawnD16Depth = 0.2f; constexpr std::uint32_t DrawnD16Bits = 13107; @@ -414,6 +416,43 @@ void BaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span Expect(output, Bits(0.5f), "D32 2D array from base slice 1 through a 2D shader image"); } +void FrameCopiesCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { + constexpr VkExtent2D extent{128, 256}; + constexpr VkDeviceSize budget = 1u << 20u; + const auto stride = AgcDriver::Graphics::DepthSliceBytes(extent, 4u); + Require(FrameCopiesOffset + 9u * stride <= BlockBytes, "the three 3-slice 128x256 D32 arrays must fit the guest block"); + const std::array<std::uint8_t*, 3> bases{block.At(FrameCopiesOffset), block.At(FrameCopiesOffset + 3u * stride), block.At(FrameCopiesOffset + 6u * stride)}; + const std::array<float, 3> expected{0.375f, 0.625f, 0.875f}; + for (std::size_t copy = 0; copy < bases.size(); ++copy) { + DrawDepth(device, reinterpret_cast<std::uintptr_t>(bases[copy]), extent, VK_FORMAT_D32_SFLOAT, DrawnDepth); + for (std::uint32_t slice = 1; slice < 3u; ++slice) FillSlice(bases[copy] + slice * stride, stride, Bits(expected[copy]), 4u); + } + const Shape shape{Format32Float, extent.width, extent.height, Type2DArray, 2, Swizzle64KbZX}; + const auto sample = [&](std::size_t copy) { + Load(device, bases[copy], shape, 2, output); + Expect(output, Bits(expected[copy]), "D32 2D array " + std::to_string(copy) + " slice 2"); + }; + AgcDriver::Graphics::Recorder::CountPresent(); + AgcDriver::Graphics::Recorder::CountPresent(); + sample(0); + sample(1); + sample(0); + sample(1); + const auto kept = AgcDriver::Graphics::DepthPlaneCopyCount(); + Require(kept == 2, std::to_string(kept) + " depth plane copies were kept after two copies over the 1 MiB budget were sampled in one present, expected both"); + const auto pair = AgcDriver::Graphics::DepthPlaneCopyBytes(); + Require(pair > budget && pair <= 2u * budget, "the two copies sampled in one present hold " + std::to_string(pair) + " bytes, the case needs more than the 1 MiB budget and no more than twice it"); + sample(2); + const auto capped = AgcDriver::Graphics::DepthPlaneCopyBytes(); + Require(capped <= 2u * budget, "the depth plane copies sampled in one present hold " + std::to_string(capped) + " bytes in " + std::to_string(AgcDriver::Graphics::DepthPlaneCopyCount()) + " copies, more than twice the 1 MiB budget"); + Require(AgcDriver::Graphics::DepthPlaneCopyCount() == 2, std::to_string(AgcDriver::Graphics::DepthPlaneCopyCount()) + " depth plane copies were kept after a third copy took the ones sampled in one present past twice the budget, expected the two most recently used"); + AgcDriver::Graphics::Recorder::CountPresent(); + AgcDriver::Graphics::Recorder::CountPresent(); + sample(1); + const auto left = AgcDriver::Graphics::DepthPlaneCopyCount(); + Require(left == 1, std::to_string(left) + " depth plane copies were kept two presents after the other copy was last sampled, expected only the newest"); +} + void ExpectRefusal(AgcDriver::VulkanDevice& device, GuestBlock& block, std::size_t offset, const Shape& shape, std::uint32_t baseArray, const std::string& expected, const std::string& what) { const auto words = TextureDescriptor(block.At(offset), shape); auto resource = AgcDriver::Graphics::DecodeTextureResource(words); @@ -441,21 +480,37 @@ int main(int argc, char** argv) { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; const std::span<std::uint32_t> output(reinterpret_cast<std::uint32_t*>(block.At(OutputOffset)), Threads); + const auto present = [&] { + if (!evicting) return; + AgcDriver::Graphics::Recorder::CountPresent(); + AgcDriver::Graphics::Recorder::CountPresent(); + }; ArrayCase(*device, block, output); + present(); CubeCase(*device, block, output); + present(); IntegerCase(*device, block, output); + present(); DepthBitsArrayCase(*device, block, output); + present(); DepthBitsBaseSliceCase(*device, block, output); + present(); GuestDepthBitsCase(*device, block, output); + present(); FirstSliceCase(*device, block, output); + present(); PaddedCubeCase(*device, block, output); + present(); SmallD16CubeCase(*device, block, output); + present(); PaddedD16CubeCase(*device, block, output); + present(); BaseSliceCase(*device, block, output); RefusalCase(*device, block); if (evicting) { const auto kept = AgcDriver::Graphics::DepthPlaneCopyCount(); Require(kept >= 1 && kept < 8, std::to_string(kept) + " depth plane copies were kept with a 1 MiB budget, expected at least the newest and fewer than the 8 an unbounded cache keeps"); + FrameCopiesCase(*device, block, output); } std::puts("depth plane sampling tests passed"); return 0; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 5f175220c4..34e4b78294 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -118,7 +118,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_CPU_STORES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | writes packet stores on the CPU instead of recording them on the GPU | | `APS5_DCC_KEYS_EACH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every DCC key store at once instead of batching them | | `APS5_DEPTH_COPY_MIN_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | minimum surface size in KiB for a depth target to take the depth-copy path | -| `APS5_DEPTH_PLANE_COPY_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | 256 | budget in MiB of the sampled depth-plane copies kept for reuse (least recently used copies leave first); `0` keeps every copy | +| `APS5_DEPTH_PLANE_COPY_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | 256 | budget in MiB of the sampled depth-plane copies kept for reuse (least recently used copies leave first; copies sampled since the previous present stay while all copies hold at most twice the budget); `0` keeps every copy | | `APS5_DEPTH_PLANE_WHOLE_STORE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | stores a stale depth plane whole when memory over it is read, instead of only the 64 KiB units the read overlaps | | `APS5_DISPATCH_CACHE_BYTES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 268435456 | size of the dispatch cache in bytes (0 keeps the default of 256 MiB) | | `APS5_DISPATCH_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 8192 | number of dispatch cache entries; 0 picks a default from the variant count | From 29e43ebd5b25304cd272bcfab2a60bde96df5047 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:32:28 +0800 Subject: [PATCH 271/702] fix(ci): preserve intentional CTest skips --- CMakeLists.txt | 3 ++ tools/check_skips.py | 5 +- tools/tests/test_check_skips_ctest.py | 66 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 tools/tests/test_check_skips_ctest.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 6aecb54402..64786e2572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -223,6 +223,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_test_registration.py --scripts) add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py) + add_test(NAME check_skips_ctest COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips_ctest.py) + set_tests_properties(check_skips_ctest PROPERTIES TIMEOUT 30) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py) add_test(NAME select_tests COMMAND ${Python3_EXECUTABLE} diff --git a/tools/check_skips.py b/tools/check_skips.py index 394cf76d0c..01ae7088ab 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -39,9 +39,10 @@ def skips(path, require_tests=False): found = False for case in root.iter("testcase"): found = True - if case.get("status") in ("disabled", "notrun"): - raise ValueError(f"{path}: testcase {case.get('name')!r} is {case.get('status')} and was not run") skipped = case.find("skipped") + ctest_skip = skipped is not None and CTEST_SKIP_STATUS.fullmatch(skipped.get("message", "").strip()) + if case.get("status") == "disabled" or (case.get("status") == "notrun" and not ctest_skip): + raise ValueError(f"{path}: testcase {case.get('name')!r} is {case.get('status')} and was not run") if skipped is None: continue name = case.get("name") diff --git a/tools/tests/test_check_skips_ctest.py b/tools/tests/test_check_skips_ctest.py new file mode 100644 index 0000000000..396c4758ba --- /dev/null +++ b/tools/tests/test_check_skips_ctest.py @@ -0,0 +1,66 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_skips + +TOOL = Path(__file__).resolve().parents[1] / "check_skips.py" + + +class IntentionalSkipTests(unittest.TestCase): + def check_real_skip(self, property_name, property_value, message, exit_code, expected): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + script = root / "skip.py" + script.write_text(f"import sys\nprint({message!r})\nsys.exit({exit_code})\n", encoding="utf-8") + (root / "CTestTestfile.cmake").write_text( + f'add_test(gpu_skip "{Path(sys.executable).as_posix()}" "{script.as_posix()}")\n' + f'set_tests_properties(gpu_skip PROPERTIES {property_name} "{property_value}")\n', encoding="utf-8") + report = root / "report.xml" + generated = subprocess.run(["ctest", "--test-dir", str(root), "--output-junit", str(report)], + capture_output=True, text=True, timeout=10) + self.assertEqual(generated.returncode, 0, generated.stdout + generated.stderr) + xml = report.read_text(encoding="utf-8") + self.assertIn('status="notrun"', xml) + self.assertIn('<skipped message="' + ("SKIP_RETURN_CODE=77" if property_name == "SKIP_RETURN_CODE" else "SKIP_REGULAR_EXPRESSION_MATCHED") + '"', xml) + for job in ("windows", "linux"): + with self.subTest(job=job): + checked = subprocess.run([sys.executable, "-I", str(TOOL), str(report), "--job", job, "--require-tests"], + capture_output=True, text=True, timeout=10) + self.assertEqual(checked.returncode, expected, checked.stdout + checked.stderr) + self.assertNotIn("Traceback", checked.stderr) + self.assertIn("all expected" if expected == 0 else "error: gpu_skip skipped", checked.stdout) + + def test_real_return_code_skip_with_accepted_reason_passes(self): + self.check_real_skip("SKIP_RETURN_CODE", "77", "skipped, no display", 77, 0) + + def test_real_regex_skip_with_accepted_reason_passes(self): + self.check_real_skip("SKIP_REGULAR_EXPRESSION", "skipped,", "skipped, no usable Vulkan device", 0, 0) + + def test_real_return_code_skip_with_unexpected_reason_fails(self): + self.check_real_skip("SKIP_RETURN_CODE", "77", "skipped, flaky", 77, 1) + + def test_real_regex_skip_with_unexpected_reason_fails(self): + self.check_real_skip("SKIP_REGULAR_EXPRESSION", "skipped,", "skipped, flaky", 0, 1) + + def test_notrun_without_ctest_skip_marker_is_rejected(self): + for marker in ('', '<skipped message="no display"/>', '<skipped message="SKIP_RETURN_CODE=77 extra"/>'): + with self.subTest(marker=marker), tempfile.TemporaryDirectory() as directory: + report = Path(directory) / "report.xml" + report.write_text(f'<testsuite><testcase name="agc_missing" status="notrun">{marker}</testcase></testsuite>', encoding="utf-8") + with self.assertRaisesRegex(ValueError, "notrun.*not run"): + list(check_skips.skips(report)) + + def test_disabled_status_is_rejected_even_with_ctest_skip_marker(self): + with tempfile.TemporaryDirectory() as directory: + report = Path(directory) / "report.xml" + report.write_text('<testsuite><testcase name="agc_disabled" status="disabled"><skipped message="SKIP_RETURN_CODE=77"/><system-out>skipped, no display</system-out></testcase></testsuite>', encoding="utf-8") + with self.assertRaisesRegex(ValueError, "disabled.*not run"): + list(check_skips.skips(report)) + + +if __name__ == "__main__": + unittest.main() From 10b21dfb3a77e8337569b6840b25d9c290e6c8a0 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 03:48:42 +0000 Subject: [PATCH 272/702] docs(libSceSaveData.native): list sceSaveDataPrepare as a silent stub and note how six titles use its result Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceSaveData.native.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index b022421794..86edd8c1d9 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -1,5 +1,9 @@ # libSceSaveData.native technical debt +### Silent stubs + +- [sceSaveDataPrepare](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) returns success without looking at its mount point or its 40-byte parameter; what it prepares on a console is unknown. Six titles call it, at eleven call sites. PPSA01494, PPSA30839 and PPSA31162 ignore the result at every site. PPSA03984 takes an error path when it is negative, PPSA34502 checks it at one of its two sites and PPSA36591 at two of its four. A failure code would change the save flow of those three titles, while the stub keeps them on the success path. + ### Unknown function info - [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state; resource parameter validation and transactional rollback semantics on host filesystem storage are unknown From e331f3b8e09113bd321fefcfa58aa4c7049b94a1 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:53:31 -0300 Subject: [PATCH 273/702] test(agc): give the read-only depth plane flush test a texture detiler TestReadOnlyStalePlaneFlush flushes a stale depth plane, which creates the plane's mirror with StorageTexture::Create(context, *context.detiler). The fixture's context has no detiler, so the reference is bound to a null pointer, which UBSan reports and stops agc_driver_graphics and agc_driver_graphics_maps_fallback on. The device always creates its detiler, so this only happens in the test. Give the test's context a detiler built over stub entry points. --- .../tests/DepthSurfaceReuse.cpp | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index cd876ad472..47bf9f8ca4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include <algorithm> #include <array> #include <cstdint> @@ -158,6 +159,21 @@ VKAPI_ATTR VkResult VKAPI_CALL mapMemory(VkDevice, VkDeviceMemory, VkDeviceSize, return VK_ERROR_MEMORY_MAP_FAILED; } VKAPI_ATTR void VKAPI_CALL unmapMemory(VkDevice, VkDeviceMemory) {} +VKAPI_ATTR VkResult VKAPI_CALL createDescriptorSetLayout(VkDevice, const VkDescriptorSetLayoutCreateInfo*, const VkAllocationCallbacks*, VkDescriptorSetLayout* layout) { + *layout = newHandle<VkDescriptorSetLayout>(); + return VK_SUCCESS; +} +VKAPI_ATTR VkResult VKAPI_CALL createPipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo*, const VkAllocationCallbacks*, VkPipelineLayout* layout) { + *layout = newHandle<VkPipelineLayout>(); + return VK_SUCCESS; +} +VKAPI_ATTR VkResult VKAPI_CALL createShaderModule(VkDevice, const VkShaderModuleCreateInfo*, const VkAllocationCallbacks*, VkShaderModule* module) { + *module = newHandle<VkShaderModule>(); + return VK_SUCCESS; +} +VKAPI_ATTR void VKAPI_CALL destroyDescriptorSetLayout(VkDevice, VkDescriptorSetLayout, const VkAllocationCallbacks*) {} +VKAPI_ATTR void VKAPI_CALL destroyPipelineLayout(VkDevice, VkPipelineLayout, const VkAllocationCallbacks*) {} +VKAPI_ATTR void VKAPI_CALL destroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL formatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { *properties = {}; @@ -216,7 +232,13 @@ PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice, const char* name) { {"vkBindBufferMemory", reinterpret_cast<PFN_vkVoidFunction>(bindBufferMemory)}, {"vkDestroyBuffer", reinterpret_cast<PFN_vkVoidFunction>(destroyBuffer)}, {"vkMapMemory", reinterpret_cast<PFN_vkVoidFunction>(mapMemory)}, - {"vkUnmapMemory", reinterpret_cast<PFN_vkVoidFunction>(unmapMemory)} + {"vkUnmapMemory", reinterpret_cast<PFN_vkVoidFunction>(unmapMemory)}, + {"vkCreateDescriptorSetLayout", reinterpret_cast<PFN_vkVoidFunction>(createDescriptorSetLayout)}, + {"vkCreatePipelineLayout", reinterpret_cast<PFN_vkVoidFunction>(createPipelineLayout)}, + {"vkCreateShaderModule", reinterpret_cast<PFN_vkVoidFunction>(createShaderModule)}, + {"vkDestroyDescriptorSetLayout", reinterpret_cast<PFN_vkVoidFunction>(destroyDescriptorSetLayout)}, + {"vkDestroyPipelineLayout", reinterpret_cast<PFN_vkVoidFunction>(destroyPipelineLayout)}, + {"vkDestroyShaderModule", reinterpret_cast<PFN_vkVoidFunction>(destroyShaderModule)} }; const auto it = table.find(name); return it == table.end() ? nullptr : it->second; @@ -467,6 +489,8 @@ void TestReadOnlyStalePlaneFlush(const Context& context) { *properties = {}; properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT; }; + TextureDetiler detiler(storageContext); + storageContext.detiler = &detiler; DepthTarget target{address, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; target.depthSwizzle = XorSwizzleMode(TextureTileMode::kZ64KBX); Require(DepthSurfaceView(storageContext, target, true) != VK_NULL_HANDLE, "a read-only depth plane must render"); From db9877a53973baffb132b90b271d5d2bd7b78a20 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 04:00:51 +0000 Subject: [PATCH 274/702] docs(libSceSaveData.native): add sceSaveDataCommit to the silent stub entry and its call sites Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceSaveData.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 86edd8c1d9..47e66286f3 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sceSaveDataPrepare](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) returns success without looking at its mount point or its 40-byte parameter; what it prepares on a console is unknown. Six titles call it, at eleven call sites. PPSA01494, PPSA30839 and PPSA31162 ignore the result at every site. PPSA03984 takes an error path when it is negative, PPSA34502 checks it at one of its two sites and PPSA36591 at two of its four. A failure code would change the save flow of those three titles, while the stub keeps them on the success path. +- [sceSaveDataPrepare, sceSaveDataCommit](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) return success without looking at their arguments; what preparing and committing a mounted save do on a console is unknown. The same six titles call both. `sceSaveDataPrepare` has 11 call sites: PPSA01494, PPSA30839 and PPSA31162 ignore its result, PPSA03984 takes an error path when it is negative, PPSA34502 checks it at one of two sites and PPSA36591 at two of four. `sceSaveDataCommit` has 14: PPSA01494 and PPSA30839 ignore its result, PPSA03984 and PPSA31162 check its sign, PPSA34502 checks it at all five sites and PPSA36591 at two of four. A failure code from either would change the save flow of the titles that check it, while the stubs keep all six on the success path. ### Unknown function info From e64d31b0b9e0a79640165fd97898a363e504cfda Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 04:04:44 +0000 Subject: [PATCH 275/702] test(agc): cover sceAgcCreateShader's relative pointers, program address patch and registration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/CMakeLists.txt | 18 +++ .../libs/prx/libSceAgc/tests/CreateShader.cpp | 117 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 core/libs/prx/libSceAgc/tests/CreateShader.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 82331aea05..cbf0e2ce92 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -488,3 +488,21 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() endif() + +add_test_executable(agc_create_shader_tests EXCLUDE_FROM_ALL + tests/CreateShader.cpp + Shader/src/CreateShader.cpp + Shader/src/ShaderUtils.cpp +) +target_include_directories(agc_create_shader_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_create_shader_tests PRIVATE libc libSceAgcDriver) +configure_windows_unwind(agc_create_shader_tests) +if(BUILD_TESTING) + set_target_properties(agc_create_shader_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_create_shader COMMAND agc_create_shader_tests) + set_tests_properties(agc_create_shader PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_create_shader PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() diff --git a/core/libs/prx/libSceAgc/tests/CreateShader.cpp b/core/libs/prx/libSceAgc/tests/CreateShader.cpp new file mode 100644 index 0000000000..90e87de37f --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/CreateShader.cpp @@ -0,0 +1,117 @@ +#include "prx/libSceAgc/Shader/include/CreateShader.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <stdexcept> + +namespace { + +const Shader* registered = nullptr; +unsigned registrations = 0; + +alignas(256) const std::array<std::uint32_t, 64> code{0xbf810000u}; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template <typename T> +T* relative(T* target, const void* field) { + return reinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(target) - reinterpret_cast<std::uintptr_t>(field)); +} + +struct Binary { + Shader shader{}; + std::array<ShaderRegister, 3> registers{}; + std::array<ShaderRegister, 1> context{}; + ShaderSpecialRegs specials{}; + ShaderUserData userData{}; + std::array<std::uint16_t, 2> direct{}; + std::array<ShaderSharp, 4> sharps{}; + std::array<ShaderSemantic, 1> inputs{}; + std::array<ShaderSemantic, 1> outputs{}; +}; + +void fill(Binary& binary, ShaderRegs::ShaderBinaryType type, std::uint32_t low) { + binary.shader.file_header = ShaderRegs::SHADER_FILE_HEADER_MAGIC; + binary.shader.version = ShaderRegs::SHADER_VERSION; + binary.shader.header_size = sizeof(Shader); + binary.shader.shader_size = sizeof(code); + binary.shader.type = static_cast<std::uint8_t>(type); + binary.registers = {{{low, 0x10u}, {low + 1u, 0xabcdef00u}, {0x20au, 0x1234u}}}; + binary.shader.num_sh_registers = static_cast<std::uint8_t>(binary.registers.size()); + binary.shader.sh_registers = relative(binary.registers.data(), &binary.shader.sh_registers); + binary.shader.specials = relative(&binary.specials, &binary.shader.specials); +} + +Shader* create(Binary& binary) { + registered = nullptr; + registrations = 0; + Shader* created = nullptr; + check(sceAgcCreateShader(&created, &binary.shader, code.data()) == 0, "sceAgcCreateShader failed"); + check(created == &binary.shader && registered == &binary.shader && registrations == 1, "the created shader is not the header, registered once"); + check(binary.shader.code == code.data(), "the code address was not stored in the header"); + return created; +} + +void testCompute() { + Binary binary; + fill(binary, ShaderRegs::ShaderBinaryType::Cs, ShaderRegs::COMPUTE_PGM_LO); + create(binary); + check(binary.shader.sh_registers == binary.registers.data() && binary.shader.specials == &binary.specials, "the relative register and special pointers were not resolved"); + check(binary.shader.user_data == nullptr && binary.shader.cx_registers == nullptr && binary.shader.input_semantics == nullptr && binary.shader.output_semantics == nullptr, "a null relative pointer did not stay null"); + const auto program = reinterpret_cast<std::uint64_t>(code.data()) + 0x1000u; + check(binary.registers[0].value == static_cast<std::uint32_t>(program >> 8u), "COMPUTE_PGM_LO does not hold the code address plus its offset"); + check(binary.registers[1].value == (0xabcdef00u | static_cast<std::uint32_t>((program >> 40u) & 0xffu)), "COMPUTE_PGM_HI did not keep its upper bits around the address byte"); + check(binary.registers[2].offset == 0x20au && binary.registers[2].value == 0x1234u, "a register other than the program address changed"); +} + +void testPixelWithUserData() { + Binary binary; + fill(binary, ShaderRegs::ShaderBinaryType::Ps, ShaderRegs::SPI_SHADER_PGM_LO_PS); + binary.shader.user_data = relative(&binary.userData, &binary.shader.user_data); + binary.shader.cx_registers = relative(binary.context.data(), &binary.shader.cx_registers); + binary.shader.num_cx_registers = 1; + binary.shader.input_semantics = relative(binary.inputs.data(), &binary.shader.input_semantics); + binary.shader.output_semantics = relative(binary.outputs.data(), &binary.shader.output_semantics); + binary.userData.direct_resource_offset = relative(binary.direct.data(), &binary.userData.direct_resource_offset); + for (std::size_t i = 0; i < binary.sharps.size(); ++i) binary.userData.sharp_resource_offset[i] = relative(&binary.sharps[i], &binary.userData.sharp_resource_offset[i]); + create(binary); + check(binary.shader.user_data == &binary.userData && binary.shader.cx_registers == binary.context.data(), "the relative user data and context register pointers were not resolved"); + check(binary.shader.input_semantics == binary.inputs.data() && binary.shader.output_semantics == binary.outputs.data(), "the relative semantic pointers were not resolved"); + check(binary.userData.direct_resource_offset == binary.direct.data(), "the user data's direct resource pointer was not resolved"); + for (std::size_t i = 0; i < binary.sharps.size(); ++i) check(binary.userData.sharp_resource_offset[i] == &binary.sharps[i], "a user data sharp resource pointer was not resolved"); + const auto program = reinterpret_cast<std::uint64_t>(code.data()) + 0x1000u; + check(binary.registers[0].value == static_cast<std::uint32_t>(program >> 8u) && binary.registers[1].value == (0xabcdef00u | static_cast<std::uint32_t>((program >> 40u) & 0xffu)), "SPI_SHADER_PGM_LO_PS and _HI_PS do not hold the code address"); +} + +void testWithoutProgramAddress() { + Binary binary; + fill(binary, ShaderRegs::ShaderBinaryType::Fs, 0x30u); + const auto registers = binary.registers; + create(binary); + check(binary.registers[0].offset == registers[0].offset && binary.registers[0].value == registers[0].value && binary.registers[1].value == registers[1].value && binary.registers[2].value == registers[2].value, "a fetch shader's registers changed"); +} + +} + +extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) { + registered = shader; + ++registrations; +} + +int main() { + try { + testCompute(); + testPixelWithUserData(); + testWithoutProgramAddress(); + std::puts("AGC create shader tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From d7afc36cf0a45febc281edebf6a4bc18abef867e Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Mon, 26 Oct 2026 23:50:23 -0400 Subject: [PATCH 276/702] test(agc): cover exact sRGB multisample DCC clears --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 25 +++++++ .../tests/execution/AlphaToCoverage.cpp | 65 +++++++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f38dfcc8a5..61160e3083 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2377,6 +2377,9 @@ if(BUILD_TESTING) add_test(NAME agc_driver_two_sample_dithered_alpha_to_coverage COMMAND agc_driver_alpha_to_coverage_tests --dithered2) set(multisampledDccClearTests) foreach(samples IN ITEMS 2 4 8) + set(testName "agc_driver_msaa_dcc_srgb_clear_${samples}") + add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-srgb-clear ${samples}) + list(APPEND multisampledDccClearTests ${testName}) set(testName "agc_driver_msaa_dcc_clear_${samples}") add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-fast-clear ${samples}) list(APPEND multisampledDccClearTests ${testName}) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 79f5d7365a..9fe51b3437 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4320,6 +4320,31 @@ void multisampleDccStateTests() { target.cmaskFastClear = true; expectFailure([&] { static_cast<void>(AgcDriver::Graphics::MultisampledDccClear(target, dcc, cmask)); }, "multisampled DCC CMASK compression or register clear"); } + for (const auto samples : {2u, 4u, 8u}) { + for (const auto format : {VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB}) { + for (const auto key : {0x00u, 0x40u, 0x80u, 0xc0u}) { + for (const auto alphaOnMsb : {false, true}) { + AgcDriver::Graphics::ColorTarget target{}; + target.extent = {64, 64}; + target.format = format; + target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.samples = samples; + target.dccAddress = 0x200000u; + target.dccPipeAligned = true; + target.dccAlphaOnMsb = alphaOnMsb; + const std::vector<std::byte> dcc(AgcDriver::Graphics::MultisampledDccBytes(target), static_cast<std::byte>(key)); + const auto clear = AgcDriver::Graphics::MultisampledDccClear(target, dcc, {}); + const auto alpha = alphaOnMsb ? 3u : format == VK_FORMAT_B8G8R8A8_SRGB ? 2u : 0u; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const bool one = channel == alpha ? key == 0x40u || key == 0xc0u : key == 0x80u || key == 0xc0u; + Require(clear.float32[channel] == static_cast<float>(one), "an sRGB multisampled DCC endpoint did not produce its exact linear clear value"); + } + } + } + } + } + } void multisampledDepthTests() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 591732ee62..947428e83d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -522,6 +522,64 @@ constexpr std::array<DitherCase, 56> TwoSampleDitherCases{{ {0x00007e00u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, }}; +void DccSrgbClearTests(AgcDriver::VulkanDevice& device, std::uint32_t samples) { + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 4); + const ColorTargetLayout allocation(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector<std::unique_ptr<Block>> blocks; + struct ClearTargets { + AgcDriver::VulkanDevice& device; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + } + } clearTargets{device}; + const auto targetFor = [&](const Block& block, std::uint32_t count, VkFormat format) { + auto target = DecodeTarget(block, count); + target.format = format; + target.elementBytes = 4; + target.bytes = layout.Bytes(); + return target; + }; + for (const auto format : {VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB}) { + for (const auto key : {0x00u, 0x40u, 0x80u, 0xc0u}) { + for (const auto alphaOnMsb : {false, true}) { + blocks.push_back(std::make_unique<Block>(allocation.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0x5a, source.bytes); + blocks.push_back(std::make_unique<Block>(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, key, metadata.bytes); + auto target = targetFor(source, samples, format); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = alphaOnMsb; + Draw(device, target, false, Float32Export, Program(Color{0.25f, 0.5f, 0.75f, 0.125f}, Float32Export, 0u), false, true); + blocks.push_back(std::make_unique<Block>(allocation.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0xa5, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = target; + pass.targets = {targetFor(resolved, 1u, format)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + const auto bytes = std::as_bytes(std::span(stored)); + std::array<std::byte, 4> expected{}; + const auto keys = key == 0x00u ? AgcDriver::Graphics::DccKeys::Clear0000 : key == 0x40u ? AgcDriver::Graphics::DccKeys::Clear0001 : key == 0x80u ? AgcDriver::Graphics::DccKeys::Clear1110 : AgcDriver::Graphics::DccKeys::Clear1111; + Require(AgcDriver::Graphics::FillDccClear(format, keys, alphaOnMsb, expected), "the sRGB DCC expected texel was not encoded"); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) Require(bytes[layout.Offset(x, y) + channel] == expected[channel], "an sRGB DCC import or resolve changed an undrawn sample endpoint"); + } + } + } + } + } +} + void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); const auto expectRange = [](std::string_view message, const char* plane, std::uint64_t address, std::size_t bytes, std::uint32_t samples) { @@ -1147,6 +1205,13 @@ int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 3 && std::string_view(argv[1]) == "--dcc-srgb-clear") { + const auto samples = std::string_view(argv[2]) == "2" ? 2u : std::string_view(argv[2]) == "4" ? 4u : std::string_view(argv[2]) == "8" ? 8u : 0u; + Require(samples != 0, "unknown sRGB DCC test sample count"); + DccSrgbClearTests(*device, samples); + std::cout << "Multisampled sRGB DCC clear tests passed\n"; + return 0; + } if ((argc == 2 || argc == 3) && std::string_view(argv[1]) == "--dcc-fast-clear") { const auto samples = argc == 2 ? 0u : std::string_view(argv[2]) == "2" ? 2u : std::string_view(argv[2]) == "4" ? 4u : std::string_view(argv[2]) == "8" ? 8u : 0u; Require(argc == 2 || samples != 0, "unknown DCC test sample count"); From c3fd587c239aa7f33cf6d21c65cf440eddfbe34c Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Mon, 26 Oct 2026 23:52:30 -0400 Subject: [PATCH 277/702] fix(agc): accept exact sRGB multisample DCC clear endpoints --- .../prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 7cb1eafdad..7eaeb33c8a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1138,7 +1138,8 @@ VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span<cons std::array<std::uint32_t, 4> pattern{}; std::memcpy(pattern.data(), texels.data(), texels.size()); VkClearColorValue clear{}; - Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC fast clear has no exact host value"); + const bool repeated = std::ranges::all_of(pattern, [&](std::uint32_t word) { return word == pattern.front(); }); + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear) || (repeated && MultisampledFillClearColor(target, pattern.front(), clear)), "multisampled DCC fast clear has no exact host value"); return clear; } From bde84e5289a6403ea6c47b0bd763c687392414bf Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 27 Oct 2026 00:04:42 -0400 Subject: [PATCH 278/702] test(agc): skip unsupported sRGB multisample counts --- .../libSceAgcDriver/tests/execution/AlphaToCoverage.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 947428e83d..ab76da6171 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -1208,7 +1208,13 @@ int main(int argc, char** argv) { if (argc == 3 && std::string_view(argv[1]) == "--dcc-srgb-clear") { const auto samples = std::string_view(argv[2]) == "2" ? 2u : std::string_view(argv[2]) == "4" ? 4u : std::string_view(argv[2]) == "8" ? 8u : 0u; Require(samples != 0, "unknown sRGB DCC test sample count"); - DccSrgbClearTests(*device, samples); + try { + DccSrgbClearTests(*device, samples); + } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; + std::printf("skipped, the device lacks the requested multisample color attachments: %s\n", error.what()); + return VulkanTestSkipped; + } std::cout << "Multisampled sRGB DCC clear tests passed\n"; return 0; } From ed2d385b9ac5365228a55534ccc3771f44d5ce4b Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 04:11:22 +0000 Subject: [PATCH 279/702] test(agc): cover the REWIND packet and its rewind state patches Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/CMakeLists.txt | 13 +++++ core/libs/prx/libSceAgc/tests/RewindPatch.cpp | 58 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 core/libs/prx/libSceAgc/tests/RewindPatch.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 82331aea05..1d61011035 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -97,6 +97,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_rewind_patch_tests EXCLUDE_FROM_ALL tests/RewindPatch.cpp) +target_include_directories(agc_rewind_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_rewind_patch_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_rewind_patch_tests) +if(BUILD_TESTING) + set_target_properties(agc_rewind_patch_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_rewind_patch COMMAND agc_rewind_patch_tests) + if(WIN32) + set_tests_properties(agc_rewind_patch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() + add_test_executable(agc_draw_index_multi_instanced_tests EXCLUDE_FROM_ALL tests/DrawIndexMultiInstanced.cpp) target_include_directories(agc_draw_index_multi_instanced_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_draw_index_multi_instanced_tests PRIVATE libSceAgc libc) diff --git a/core/libs/prx/libSceAgc/tests/RewindPatch.cpp b/core/libs/prx/libSceAgc/tests/RewindPatch.cpp new file mode 100644 index 0000000000..bdeb5cbd5e --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/RewindPatch.cpp @@ -0,0 +1,58 @@ +#include "prx/libSceAgc/DcbFlow/include/Control.hpp" +#include "prx/libSceAgc/Patch/include/Rewind.hpp" + +#include <array> +#include <cstdint> +#include <cstdio> +#include <stdexcept> + +namespace { + +constexpr std::uint32_t Sentinel = 0xabcdef01u; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +struct Storage { + std::array<std::uint32_t, 8> words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + + Storage() { + words.fill(Sentinel); + } +}; + +void testRewind() { + check(sceAgcDcbRewindGetSize() == 8, "REWIND packet size"); + for (std::uint32_t state : {0u, 1u}) { + Storage storage; + auto* packet = sceAgcDcbRewind(&storage.buffer, state); + check(packet == storage.words.data() && packet[0] == 0xc0005900u && packet[1] == state << 31u, "REWIND does not carry the initial state in bit 31"); + check(storage.buffer.cursor_up == packet + sceAgcDcbRewindGetSize() / sizeof(std::uint32_t), "REWIND did not advance the cursor by its size"); + check(storage.words[2] == Sentinel, "REWIND wrote past its two words"); + } +} + +void testRewindStatePatch() { + Storage storage; + auto* packet = sceAgcDcbRewind(&storage.buffer, 0); + check(sceAgcRewindPatchSetRewindState(packet, 1) == 0 && packet[0] == 0xc0005900u && packet[1] == 0x80000000u, "the rewind state patch did not set bit 31"); + check(sceAgcRewindPatchSetRewindState(packet, 0) == 0 && packet[0] == 0xc0005900u && packet[1] == 0, "the rewind state patch did not clear bit 31"); + check(sceAgcAsyncRewindPatchSetRewindState(packet, 1) == 0 && packet[1] == 0x80000000u, "the asynchronous rewind state patch did not set bit 31"); + check(storage.words[2] == Sentinel, "a rewind state patch wrote past the packet"); +} + +} + +int main() { + try { + testRewind(); + testRewindStatePatch(); + std::puts("AGC rewind tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From 9c3ad7d55d86478e5cb4c3fbbee670a296425a56 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 04:13:11 +0000 Subject: [PATCH 280/702] test(agc): cover the graphics and compute ATOMIC_MEM packets Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/CMakeLists.txt | 13 +++++ core/libs/prx/libSceAgc/tests/AtomicMem.cpp | 63 +++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 core/libs/prx/libSceAgc/tests/AtomicMem.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 82331aea05..db23fe60bf 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -109,6 +109,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() endif() + +add_test_executable(agc_atomic_mem_tests EXCLUDE_FROM_ALL tests/AtomicMem.cpp) +target_include_directories(agc_atomic_mem_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_atomic_mem_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_atomic_mem_tests) +if(BUILD_TESTING) + set_target_properties(agc_atomic_mem_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_atomic_mem COMMAND agc_atomic_mem_tests) + if(WIN32) + set_tests_properties(agc_atomic_mem PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() configure_windows_unwind(libSceAgc) add_test_executable(agc_static_buffer_tests EXCLUDE_FROM_ALL tests/StaticBuffer.cpp) diff --git a/core/libs/prx/libSceAgc/tests/AtomicMem.cpp b/core/libs/prx/libSceAgc/tests/AtomicMem.cpp new file mode 100644 index 0000000000..b98e83ce99 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/AtomicMem.cpp @@ -0,0 +1,63 @@ +#include "prx/libSceAgc/DcbFlow/include/Memory.hpp" +#include "prx/libSceAgc/Acb/include/Memory.hpp" + +#include <algorithm> +#include <array> +#include <cstdint> +#include <cstdio> +#include <stdexcept> + +namespace { + +constexpr std::uint32_t Sentinel = 0xabcdef01u; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +struct Storage { + std::array<std::uint32_t, 16> words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + + Storage() { + words.fill(Sentinel); + } +}; + +using Writer = std::uint32_t* (APS5_VABI *)(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint8_t, const volatile void*, std::uint64_t, std::uint64_t, std::uint16_t); + +void testPacket(Writer writer, std::uint32_t size, std::uint8_t atomicOp, std::uintptr_t address) { + check(size == 36, "ATOMIC_MEM packet size"); + Storage storage; + auto* packet = writer(&storage.buffer, atomicOp, 0x5, 2, reinterpret_cast<const volatile void*>(address), 0x1122334455667788ull, 0x99aabbccddeeff00ull, 0x1abc); + const std::array<std::uint32_t, 9> expected{ + 0xc0071e00u, + atomicOp | (0x5u << 8u) | (2u << 25u), + static_cast<std::uint32_t>(address), + static_cast<std::uint32_t>(address >> 32u), + 0x55667788u, + 0x11223344u, + 0xddeeff00u, + 0x99aabbccu, + 0x1abcu, + }; + check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), packet), "incorrect ATOMIC_MEM packet"); + check(storage.buffer.cursor_up == packet + size / sizeof(std::uint32_t), "the cursor did not advance by the packet size"); + check(std::all_of(storage.words.begin() + expected.size(), storage.words.end(), [](std::uint32_t word) { return word == Sentinel; }), "ATOMIC_MEM wrote past its nine words"); +} + +} + +int main() { + try { + testPacket(sceAgcDcbAtomicMem, sceAgcDcbAtomicMemGetSize(), 0x2f, 0x0000123456789ab4ull); + testPacket(sceAgcDcbAtomicMem, sceAgcDcbAtomicMemGetSize(), 0x6f, 0x0000123456789ab8ull); + testPacket(sceAgcAcbAtomicMem, sceAgcAcbAtomicMemGetSize(), 0x2f, 0x0000123456789ab4ull); + testPacket(sceAgcAcbAtomicMem, sceAgcAcbAtomicMemGetSize(), 0x6f, 0x0000123456789ab8ull); + std::puts("AGC atomic memory tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From be7db251ad005c9ead50177d190b9f04a17743f9 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:36:06 -0300 Subject: [PATCH 281/702] fix(agc): check the deferred destruction resolver only against live devices The deferred destruction registry kept the first device's vkGetDeviceProcAddr after that device was destroyed. VulkanDevice unloads the Vulkan loader with the device, and on macOS the next load can map it at another address, so every later device in the process failed with "deferred destruction found two different device resolvers". The check now applies only while another device is still registered; the new device's resolver replaces the old one. --- .../Graphics/src/DeferredDestruction.cpp | 2 +- .../tests/DeferredDestruction.cpp | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp index 1fb049628a..205b8dbf34 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp @@ -81,7 +81,7 @@ PFN_vkGetDeviceProcAddr DeferDestructionToIdle(VkDevice device, PFN_vkGetDeviceP auto& state = registry(); std::lock_guard lock(state.mutex); const auto current = state.resolver.load(std::memory_order_acquire); - if (current != nullptr && current != resolver) throw std::runtime_error("AGC graphics: deferred destruction found two different device resolvers"); + if (current != nullptr && current != resolver && !state.devices.empty()) throw std::runtime_error("AGC graphics: deferred destruction found two different device resolvers"); if (!state.devices.emplace(device, std::move(deferred)).second) throw std::runtime_error("AGC graphics: deferred destruction was set up twice for one device"); state.resolver.store(resolver, std::memory_order_release); return &deferringDeviceProc; diff --git a/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp b/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp index 7873086b80..39c3d02ad0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp @@ -51,6 +51,16 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { return nullptr; } +VKAPI_ATTR VkResult VKAPI_CALL reloadedQueueWaitIdle(VkQueue queue) { + events.push_back(Name("reloaded idle ", queue)); + return VK_SUCCESS; +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL reloadedProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkQueueWaitIdle") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&reloadedQueueWaitIdle); + return mockProc(device, name); +} + Context ContextFor(VkDevice device, PFN_vkGetDeviceProcAddr proc) { Context context{}; context.device = device; @@ -115,12 +125,35 @@ void OtherDevicesDestroyAtOnce() { DestroyDeferredObjects(device); } +PFN_vkGetDeviceProcAddr TrySetUp(VkDevice device, PFN_vkGetDeviceProcAddr resolver) { + try { + return DeferDestructionToIdle(device, resolver); + } catch (const std::exception&) { + return nullptr; + } +} + +void ALaterDeviceUsesItsOwnResolver() { + events.clear(); + const auto first = Handle<VkDevice>(0x103); + DeferDestructionToIdle(first, mockProc); + DestroyDeferredObjects(first); + const auto second = Handle<VkDevice>(0x104); + const auto proc = TrySetUp(second, reloadedProc); + Expect(proc != nullptr, "a device set up after the last one went could not use a resolver from a reloaded Vulkan loader"); + if (proc == nullptr) return; + Expect(ContextFor(second, proc).Function<PFN_vkQueueWaitIdle>("vkQueueWaitIdle") == &reloadedQueueWaitIdle, "a later device did not pass through to its own resolver"); + Expect(TrySetUp(Handle<VkDevice>(0x105), mockProc) == nullptr, "two live devices with different resolvers were accepted"); + DestroyDeferredObjects(second); +} + } int main() { try { DeferredUntilTheQueueIsIdle(); OtherDevicesDestroyAtOnce(); + ALaterDeviceUsesItsOwnResolver(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; From 54143904099eee1dd7bd7c45125e581872cc7db0 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:16:34 -0300 Subject: [PATCH 282/702] test(agc): load the Vulkan loader by its macOS name in the host import and producer gate helpers The guest memory lifetime tests' host import query and the indirect arguments pending producer gate opened libvulkan.so.1 on every non-Windows host, so on macOS they failed with "cannot load Vulkan" before running. They now open libvulkan.1.dylib there, as the other driver tests do. --- .../prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp | 2 ++ .../tests/execution/IndirectArgumentsPendingProducer.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp index 863b8ca67f..2b2866adc8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp @@ -17,6 +17,8 @@ static inline bool GuestMemoryLifetimeHasHostImports(VkDevice device) { } #ifdef _WIN32 constexpr const char* libraryName = "vulkan-1.dll"; +#elif defined(__APPLE__) + constexpr const char* libraryName = "libvulkan.1.dylib"; #else constexpr const char* libraryName = "libvulkan.so.1"; #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp index 306f35cbd0..eeb642dcbc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp @@ -162,6 +162,8 @@ public: explicit ProducerSubmissionGate(AgcDriver::VulkanDevice& device) : device(device) { #ifdef _WIN32 constexpr const char* libraryName = "vulkan-1.dll"; +#elif defined(__APPLE__) + constexpr const char* libraryName = "libvulkan.1.dylib"; #else constexpr const char* libraryName = "libvulkan.so.1"; #endif From a265527c4d31c7aadbe83d67e0ebb5859d11ad80 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 04:18:57 +0000 Subject: [PATCH 283/702] test(libSceFont): cover the terminate code and position a font string records Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestFont.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 093c88fe45..007e863d73 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -87,6 +87,18 @@ static std::int32_t APS5_VABI ParseText(FontTextSource* source, void** order, Fo return 1; } +static std::int32_t APS5_VABI ParseLine(FontTextSource* source, void** order, FontTextParseResult* result) { + auto* parsed = static_cast<ParsedText*>(source->textObject); + *order = const_cast<char*>(parsed->text + parsed->at); + if (parsed->text[parsed->at] == '\n') { + result->Terminate.terminateCode = '\n'; + return 0; + } + result->FontCode.font = parsed->font; + result->FontCode.code = static_cast<unsigned char>(parsed->text[parsed->at++]); + return 1; +} + struct ParsedCodes { const std::uint32_t* codes; std::size_t count; @@ -116,6 +128,16 @@ static void CheckTextCodes(const FontMemory* memory, FontHandle font) { FontTextCharacter* characters = sceFontStringRefersTextCharacters(string, &count); Require(characters != nullptr && count == 3); Require(sceFontStringGetWritingForm(string) == 0x12); + Require(sceFontStringGetTerminateCode(string) == 0 && sceFontStringGetTerminateOrder(string) == parsed.text + 3); + ParsedText line{"AV\nA", 0, font}; + FontTextSource lineSource{}; + Require(sceFontTextSourceInit(&lineSource, line.text, 4, ParseLine, &line) == SCE_FONT_OK); + FontString lineString = nullptr; + Require(sceFontCreateString(memory, &lineSource, nullptr, &lineString) == SCE_FONT_OK && lineString != nullptr); + std::uint32_t lineCount = 0; + Require(sceFontStringRefersTextCharacters(lineString, &lineCount) != nullptr && lineCount == 2); + Require(sceFontStringGetTerminateCode(lineString) == '\n' && sceFontStringGetTerminateOrder(lineString) == line.text + 2); + Require(sceFontDestroyString(&lineString) == SCE_FONT_OK); FontString rtlString = nullptr; Require(sceFontTextSourceSetWritingForm(&source, 0x11) == SCE_FONT_OK && sceFontCreateString(memory, &source, nullptr, &rtlString) == SCE_FONT_OK); Require(sceFontStringGetWritingForm(rtlString) == 0x11); From bcb3df7352594dd8743aafd057e4ad02530bfa6a Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Mon, 26 Oct 2026 22:39:19 +0000 Subject: [PATCH 284/702] docs(agc): note how seven titles call sceAgcDebugRaiseException Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAgc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 7393c238c5..275abc2cf2 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -41,7 +41,7 @@ - [sceAgcDcbDrawIndexMultiInstanced](../../../core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp) (libSceAgc) - what the arguments mean is not known. The function writes the console's 15 dwords (firmware 13.40, measured in a retail title): tag 0xc6000006, `DRAW_INDEX_MULTI_INST` (0x3a) with the first three arguments in order and the fourth twice (the first copy at least 1), initiator 0x80 whatever the modifier, then tag 0xc6000000; it checks no argument. The parameter names (index count, index address, object IDs, instance count) are unverified: AMD's PFP layout of the packet (instance count, object ID address, max size, index base, index count, initiator) would make them instance count, object ID address, index address and index count. The driver refuses the packet. ### Functional -- [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, and a second argument is not declared: OpenAGC declares no arguments, but PPSA21564 01.007.000 passes two, 1 in `edi` and 0 in `esi`, from the callback its command buffers get when none is given (0x6f3b6a0), and ignores the result. The second argument's type and meaning are not identified +- [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, and a second argument is not declared: OpenAGC declares no arguments, but PPSA21564 01.007.000 passes two, 1 in `edi` and 0 in `esi`, from the callback its command buffers get when none is given (0x6f3b6a0), and ignores the result. The second argument's type and meaning are not identified. PPSA01494, PPSA03984, PPSA18194, PPSA30839, PPSA31162 and PPSA34502 call it the same way, once each, with 1 and 0 and the result ignored. - [sceAgcSetAmmSemaphoreMemory, sceAgcSetSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the checks, error codes and clearing are the firmware's, but two firmware steps are not reproduced. After its checks, the firmware validates the memory with libkernel `qSleeVC6lwI`: an ioctl `0xc018ac15` taking `{memory, size, 1}`. The AMM call always does this; the non-AMM call only for memory from 8 TiB up that ends below 15 TiB. The firmware then sends `{7: memory, 8: size, 0xb: 0 for AMM, 1 otherwise}` through sceAgcDriverPassInfoDownward. Neither step happens here, so a registration that the kernel or driver would refuse succeeds. A null memory with a non-zero size and a range that wraps the address space throw where the firmware would fault - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - 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. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting From d31f194c9c9c0cee54ecfd20a54b3b61890a469c Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 04:24:02 +0000 Subject: [PATCH 285/702] test(agc): keep the DMA data patch checks to valid packets Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/tests/DmaData.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/libs/prx/libSceAgc/tests/DmaData.cpp b/core/libs/prx/libSceAgc/tests/DmaData.cpp index 602b7cd7d3..2000994c62 100644 --- a/core/libs/prx/libSceAgc/tests/DmaData.cpp +++ b/core/libs/prx/libSceAgc/tests/DmaData.cpp @@ -92,20 +92,6 @@ void testPatches() { check(sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(packet, 0xfedcba9876543210ull) == 0 && packet[2] == 0x76543210u && packet[3] == 0xfedcba98u, "the source patch did not keep a 64-bit immediate whole"); } - Storage refused; - auto* packet = writeDraw(&refused.buffer); - packet[0] = 0xc0051000u; - auto before = refused.words; - expectFailure([&] { sceAgcDmaDataPatchSetDstAddressOrOffset(packet, PatchedDestination); }); - expectFailure([&] { sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(packet, PatchedSource); }); - check(refused.words == before, "a patch of a NOP changed it"); - packet[0] = 0xc0045000u; - before = refused.words; - expectFailure([&] { sceAgcDmaDataPatchSetDstAddressOrOffset(packet, PatchedDestination); }); - expectFailure([&] { sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(packet, PatchedSource); }); - check(refused.words == before, "a patch of a short DMA packet changed it"); - expectFailure([] { sceAgcDmaDataPatchSetDstAddressOrOffset(nullptr, PatchedDestination); }); - expectFailure([] { sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(nullptr, PatchedSource); }); } } From ad95db103265567457c7c058dcce63366d3e0c92 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:56:54 +0100 Subject: [PATCH 286/702] docs(agc): note the 0x1aa01 DB_ALPHA_TO_MASK stop of PPSA12361 PPSA12361 reaches DB_ALPHA_TO_MASK 0x1aa01 on main 74acfad65b on an RTX 4080 SUPER and an RX 6400. It is the accepted 0xaa01 with bit 16 also set, and radeonsi never programs that combination. Its semantics are not measured, so the path keeps throwing; the entry records it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 170251ded9..1699ef79d5 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -46,6 +46,7 @@ - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `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. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. Two-sample `DB_ALPHA_TO_MASK = 0xe701` (Bugsnax, PPSA01503) uses an eight-level table measured on gfx1032 with the locations (4, 4), (-4, -4) and (-4, 4), (4, -4), not on gfx1013; other sample counts, locations and exports throw, and `MASK_EXPORT_NUM_SAMPLES` 0 is accepted though measured at 1 - [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs (with a one-sample depth target, `ANCILLARY` only under `PS_ITER_SAMPLE`), because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. A pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. With a one-sample depth target and without `PS_ITER_SAMPLE`, the [packed ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) gives a program that reads `ANCILLARY` sample index 0, inferred for a pixel the guest does not iterate, and that is the sample the one-sample draw keeps. Points and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA.INTERPOLATE_COMP_Z` is accepted: with every sample at the pixel center, Z interpolated to any sample position is the center's Z, which the one-sample draw tests. That rests on the same inference; neither [PAL](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MsaaState.cpp#L117-L118) nor [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/761d5c8a54f93b3d43608f718910ed742983dd0b/src/gallium/drivers/radeonsi/si_state.c#L3314-L3316) sets the bit, and its meaning is not documented. Line and point lists (`VGT_PRIMITIVE_TYPE` 1-3, even when a geometry program turns them into triangles), `INTERPOLATE_SRC_Z` and overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) still throw. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. Gran Turismo 7 (PPSA01317) draws its main pass with a one-sample depth target, `DB_EQAA` 0x00050000 and a 4x configuration. - [One-fragment multisampled color target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a color target with `CB_COLOR_ATTRIB` 4 samples and 1 fragment is decoded as one sample that keeps its coverage sample count and draws sample 0's coverage. Measured only on radeonsi GL (Radeon 610M and 680M); console behaviour is not measured. It requires static `DB_EQAA` anchors at the depth target's sample count and CMASK code 0 or 0xc-0xf. More than one but fewer fragments than samples, more fragments than samples, other CMASK codes and any guest read of the FMASK of a target the host drew still throw. +- [Alpha-to-coverage `0x1aa01`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `DB_ALPHA_TO_MASK = 0x1aa01` throws `dithered or rounded alpha-to-coverage is unsupported`. It differs from the accepted undithered `0xaa01` by bit 16 (`0x10000`), which the accepted dithered `0x18701` also has: [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs `OFFSET_ROUND(1)` only with the dithered offsets and `OFFSET_ROUND(0)` for `0xaa01`, so it never programs this combination. PPSA12361 reaches it on main 74acfad65b on an RTX 4080 SUPER (`run_024721.err` lines 3484-3486) and an RX 6400 (`run_025512.err` lines 2667-2671). What the console covers for this value is not measured, so it stays unsupported. - [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 or rectangles (`VGT_PRIMITIVE_TYPE` 7 or 17 as well as the triangle types) 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. EA Sports UFC 5 (PPSA03541) reaches a rect-list draw in this state. Other locations of sample 0, absent location registers, points, lines, alpha-to-coverage and conservative rasterization throw - [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - not measured on hardware: `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear puts every color, depth and stencil sample at the pixel center, as in GL without multisampling ([ARB_multisample](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089/extensions/ARB/ARB_multisample.txt#L185), [RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11)). Lines cover whole pixels by the host's line rule, not the console's aliased one ([D3D11.3](https://github.com/microsoft/DirectX-Specs/blob/f37b0d6033a1/d3d/archive/D3D11_3_FunctionalSpec.htm#L13705)). Depth sample tests and occlusion counts are unknown. Rasterized points, alpha-to-coverage, and Z interpolation, overrasterization or a sample-mask export with depth throw. PPSA06888 draws a 2x target, PPSA18216 a 4x depth and color pair and 4x lines. - [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 From 152155e484d88538014c60caab505ae82a7ee552 Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Tue, 27 Oct 2026 01:30:11 -0300 Subject: [PATCH 287/702] fix(libkernel): isolate AIO controls in the registry test target --- core/libs/prx/libkernel/Aio/src/Aio.cpp | 19 +- core/libs/prx/libkernel/CMakeLists.txt | 12 +- core/libs/prx/libkernel/include/Api.hpp | 2 - core/libs/tests/AioTest.hpp | 14 ++ core/libs/tests/GuestAio.cpp | 212 -------------------- core/libs/tests/GuestAioRegistry.cpp | 251 ++++++++++++++++++++++++ 6 files changed, 294 insertions(+), 216 deletions(-) create mode 100644 core/libs/tests/AioTest.hpp create mode 100644 core/libs/tests/GuestAioRegistry.cpp diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index 0e108b7b72..e228a480e5 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -1,6 +1,5 @@ #include "prx/libkernel/include/Api.hpp" #include <array> -#include <atomic> #include <cerrno> #include <chrono> #include <cstddef> @@ -18,6 +17,11 @@ #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" +#ifdef ANYPS5_AIO_TESTING +#include <atomic> +#include "tests/AioTest.hpp" +#endif + #ifndef _WIN32 #include <fcntl.h> #include <unistd.h> @@ -50,6 +54,8 @@ struct RequestState { }; std::array<RequestState, AioMaxQueues - 1> g_requests{}; std::int64_t g_nextId = 1; + +#ifdef ANYPS5_AIO_TESTING std::atomic<std::int32_t> g_processingWaiters{0}; class ProcessingWaiter { @@ -67,6 +73,7 @@ public: private: bool observed = false; }; +#endif RequestState* FindLocked(std::int32_t id) { if (id <= 0) return nullptr; @@ -249,6 +256,7 @@ int ValidateIds(const std::int32_t* id, std::int32_t num, const void* out) { extern "C" { +#ifdef ANYPS5_AIO_TESTING std::int32_t AioProcessingWaitersForTest_nid_no_patch() { return g_processingWaiters.load(); } @@ -259,6 +267,7 @@ void AioAdvanceIdentityForTest_nid_no_patch(std::int64_t next) { throw std::invalid_argument("AIO: invalid test identity advance"); g_nextId = next; } +#endif int APS5_VABI sceKernelAioDeleteRequest(int32_t id, int32_t* ret) { if (ret == nullptr) return SCE_KERNEL_ERROR_EFAULT; @@ -350,7 +359,9 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, if (error != 0) return error; if (mode != AioWaitAnd && mode != AioWaitOr) return SCE_KERNEL_ERROR_EINVAL; const auto start = std::chrono::steady_clock::now(); +#ifdef ANYPS5_AIO_TESTING ProcessingWaiter waiter; +#endif for (;;) { bool allDone = true; bool anyCompleted = false; @@ -366,7 +377,9 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, return 0; } } +#ifdef ANYPS5_AIO_TESTING waiter.Observe(); +#endif if (usec != nullptr && *usec != 0) { const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count(); if (elapsed > static_cast<std::int64_t>(*usec)) { @@ -382,7 +395,9 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; const auto start = std::chrono::steady_clock::now(); +#ifdef ANYPS5_AIO_TESTING ProcessingWaiter waiter; +#endif for (;;) { std::int32_t current; { @@ -393,7 +408,9 @@ int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec *state = current; return 0; } +#ifdef ANYPS5_AIO_TESTING waiter.Observe(); +#endif if (usec != nullptr && *usec != 0) { const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count(); if (elapsed > static_cast<std::int64_t>(*usec)) { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1652ff91ec..d9880edc07 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -502,8 +502,18 @@ if(BUILD_TESTING) target_link_libraries(guest_aio_tests PRIVATE libkernel libc) configure_windows_unwind(guest_aio_tests) add_test(NAME guest_aio COMMAND guest_aio_tests) + add_test_executable(guest_aio_registry_tests + ${LIBS_INCLUDE_DIR}/tests/GuestAioRegistry.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Aio/src/Aio.cpp) + target_include_directories(guest_aio_registry_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_definitions(guest_aio_registry_tests PRIVATE ANYPS5_AIO_TESTING) + target_link_libraries(guest_aio_registry_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_aio_registry_tests) + if(WIN32) + target_compile_definitions(guest_aio_registry_tests PRIVATE _WIN32_WINNT=0x0602) + endif() foreach(mode identity admission rollback identity-exhaustion processing processing-rollback) - add_test(NAME guest_aio_${mode} COMMAND guest_aio_tests ${mode}) + add_test(NAME guest_aio_${mode} COMMAND guest_aio_registry_tests ${mode}) set_tests_properties(guest_aio_${mode} PROPERTIES TIMEOUT 20) if(WIN32) set_tests_properties(guest_aio_${mode} PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index c20a1ee0d9..e36c9a773d 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -189,8 +189,6 @@ int APS5_VABI sceKernelAioPollRequest(int32_t id, int32_t* state); int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, uint32_t mode, uint32_t* usec); int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec); int APS5_VABI sceKernelAioPollRequests(int32_t* id, int32_t num, int32_t* state); -void AioAdvanceIdentityForTest_nid_no_patch(std::int64_t next); -std::int32_t AioProcessingWaitersForTest_nid_no_patch(); int APS5_VABI sceKernelAprResolveFilepathsToIds(const char** paths, uint32_t count, uint32_t* ids, uint32_t* errorIndex); int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* errorIndex); int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* errorIndex); diff --git a/core/libs/tests/AioTest.hpp b/core/libs/tests/AioTest.hpp new file mode 100644 index 0000000000..4ec41e60ff --- /dev/null +++ b/core/libs/tests/AioTest.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include <cstdint> + +#ifndef ANYPS5_AIO_TESTING +#error AIO test controls require ANYPS5_AIO_TESTING +#endif + +extern "C" { + +void AioAdvanceIdentityForTest_nid_no_patch(std::int64_t next); +std::int32_t AioProcessingWaitersForTest_nid_no_patch(); + +} diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 1cb4297940..83fc668a6e 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -4,19 +4,15 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/KernelErrors.hpp" -#include "prx/libc/include/GuestArena.hpp" #include <array> #include <atomic> #include <chrono> -#include <condition_variable> #include <cstdio> #include <cstdlib> #include <cstring> #include <filesystem> #include <fstream> #include <iterator> -#include <limits> -#include <mutex> #include <stdexcept> #include <string> #include <thread> @@ -39,208 +35,6 @@ static void DeleteAio(std::int32_t id) { Check(sceKernelAioDeleteRequest(id, &removed) == 0 && removed == 0); } -static std::int32_t SubmitEmptyAio() { - KernelAioResult result{-1, 0}; - KernelAioRwRequest request{0, 0, nullptr, &result, -1}; - std::int32_t id = 0; - Check(sceKernelAioSubmitReadCommands(&request, 1, 1, &id) == 0); - Check(result.state == 3 && result.return_value == SCE_KERNEL_ERROR_EBADF); - return id; -} - -static void CheckAioIdentity() { - const auto retained = SubmitEmptyAio(); - std::int32_t state = 0; - Check(sceKernelAioPollRequest(retained, &state) == 0 && state == 3); - auto previous = retained; - for (int iteration = 0; iteration < 1100; ++iteration) { - const auto id = SubmitEmptyAio(); - Check(id > previous); - previous = id; - DeleteAio(id); - Check(sceKernelAioPollRequest(retained, &state) == 0 && state == 0x10003); - } - DeleteAio(retained); - for (int iteration = 0; iteration < 1100; ++iteration) { - const auto id = SubmitEmptyAio(); - Check(id > previous); - previous = id; - DeleteAio(id); - } - Check(sceKernelAioPollRequest(retained, &state) == 0 && state == SCE_KERNEL_ERROR_ESRCH); -} - -static void CheckAioAdmission() { - const auto file = std::filesystem::path("anyps5-aio-admission-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); - { std::ofstream stream(file, std::ios::binary); stream << 'Z'; Check(static_cast<bool>(stream)); } - const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); - Check(fd >= 0); - std::vector<std::int32_t> retained; - for (int index = 0; index < 510; ++index) retained.push_back(SubmitEmptyAio()); - std::array<KernelAioResult, 2> results{{{-7, 7}, {-7, 7}}}; - char byte = 'X'; - KernelAioRwRequest requests[2]{{0, 1, &byte, &results[0], fd}, {0, 1, &byte, &results[1], fd}}; - std::int32_t state = 0; - Check(sceKernelAioPollRequest(retained[0], &state) == 0 && state == 3); - for (const auto submit : {sceKernelAioSubmitReadCommandsMultiple, sceKernelAioSubmitWriteCommandsMultiple}) { - std::int32_t ids[2]{-7, -7}; - bool refused = false; - try { submit(requests, 2, 1, ids); } catch (const std::runtime_error&) { refused = true; } - Check(refused && ids[0] == -7 && ids[1] == -7); - Check(results[0].state == 7 && results[0].return_value == -7 && results[1].state == 7 && results[1].return_value == -7); - } - retained.push_back(SubmitEmptyAio()); - for (const auto submit : {sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands}) { - std::int32_t id = -7; - bool refused = false; - try { submit(requests, 1, 1, &id); } catch (const std::runtime_error&) { refused = true; } - Check(refused && id == -7 && results[0].state == 7 && results[0].return_value == -7); - } - Check(sceKernelAioPollRequest(retained[0], &state) == 0 && state == 0x10003); - { std::ifstream stream(file, std::ios::binary); Check(stream.get() == 'Z' && stream.get() == std::char_traits<char>::eof()); } - for (const auto id : retained) DeleteAio(id); - std::int32_t ids[2]{}; - Check(sceKernelAioSubmitReadCommandsMultiple(requests, 2, 1, ids) == 0); - Check(ids[0] != ids[1] && results[0].state == 3 && results[1].state == 3); - for (const auto id : ids) DeleteAio(id); - Check(sceKernelClose(fd) == 0 && std::filesystem::remove(file)); -} - -static int g_aioSourceCalls = 0; - -static void FailAioSource(std::uintptr_t, std::size_t) { - if (++g_aioSourceCalls % 2 == 0) throw std::runtime_error("AIO source fault injection"); -} - -static void CheckAioRollback() { - const auto file = std::filesystem::path("anyps5-aio-rollback-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); - { std::ofstream stream(file, std::ios::binary); stream << "zz"; Check(static_cast<bool>(stream)); } - const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); - Check(fd >= 0); - std::array<KernelAioResult, 2> results{{{-7, 7}, {-7, 7}}}; - char byte = 'A'; - KernelAioRwRequest requests[2]{{0, 1, &byte, &results[0], fd}, {1, 1, &byte, &results[1], fd}}; - for (const auto submit : {sceKernelAioSubmitWriteCommands, sceKernelAioSubmitWriteCommandsMultiple}) { - GuestArena::GuestArenaSetHostReadHook_nid_no_patch(FailAioSource); - for (int iteration = 0; iteration < 600; ++iteration) { - results = {{{-7, 7}, {-7, 7}}}; - std::int32_t ids[2]{-7, -7}; - bool refused = false; - const auto calls = g_aioSourceCalls; - try { submit(requests, 2, 1, ids); } catch (const std::runtime_error& error) { refused = std::strcmp(error.what(), "AIO source fault injection") == 0; } - Check(refused && ids[0] == -7 && ids[1] == -7); - Check(g_aioSourceCalls == calls + 2 && results[0].state == 3 && results[0].return_value == 1 && results[1].state == 7); - } - GuestArena::GuestArenaSetHostReadHook_nid_no_patch(nullptr); - } - Check(sceKernelClose(fd) == 0); - { std::ifstream stream(file, std::ios::binary); Check(stream.get() == 'A' && stream.get() == 'z' && stream.get() == std::char_traits<char>::eof()); } - Check(std::filesystem::remove(file)); - CheckAioAdmission(); -} - -static void CheckAioIdentityExhaustion() { - const auto maximum = std::numeric_limits<std::int32_t>::max(); - AioAdvanceIdentityForTest_nid_no_patch(maximum); - std::array<KernelAioResult, 2> results{{{-7, 7}, {-7, 7}}}; - KernelAioRwRequest requests[2]{{0, 0, nullptr, &results[0], -1}, {0, 0, nullptr, &results[1], -1}}; - std::int32_t ids[2]{-7, -7}; - bool refused = false; - try { sceKernelAioSubmitReadCommandsMultiple(requests, 2, 1, ids); } catch (const std::runtime_error&) { refused = true; } - Check(refused && ids[0] == -7 && ids[1] == -7 && results[0].state == 7 && results[1].state == 7); - const auto final = SubmitEmptyAio(); - Check(final == maximum); - DeleteAio(final); - refused = false; - try { sceKernelAioSubmitReadCommands(requests, 1, 1, ids); } catch (const std::runtime_error&) { refused = true; } - Check(refused && ids[0] == -7 && results[0].state == 7); - std::int32_t state = 0; - Check(sceKernelAioPollRequest(final, &state) == 0 && state == SCE_KERNEL_ERROR_ESRCH); -} - -static std::mutex g_aioGateMutex; -static std::condition_variable g_aioGateChanged; -static bool g_aioGateEntered = false; -static bool g_aioGateReleased = false; -static bool g_aioGateFails = false; - -static void HoldAioSource(std::uintptr_t, std::size_t) { - std::unique_lock lock(g_aioGateMutex); - g_aioGateEntered = true; - g_aioGateChanged.notify_all(); - Check(g_aioGateChanged.wait_for(lock, std::chrono::seconds(5), [] { return g_aioGateReleased; })); - if (g_aioGateFails) throw std::runtime_error("AIO gated transfer failure"); -} - -static void CheckAioProcessingOwnership(bool fail) { - g_aioGateFails = fail; - const auto maximum = std::numeric_limits<std::int32_t>::max(); - AioAdvanceIdentityForTest_nid_no_patch(maximum - 2); - const auto completed = SubmitEmptyAio(); - const auto pending = completed + 1; - KernelAioResult result{-7, 7}; - char byte = 'x'; - KernelAioRwRequest request{0, 1, &byte, &result, -1}; - std::int32_t submittedId = -7; - bool submitFailed = false; - GuestArena::GuestArenaSetHostReadHook_nid_no_patch(HoldAioSource); - std::thread submitter([&] { - try { Check(sceKernelAioSubmitWriteCommands(&request, 1, 1, &submittedId) == 0); } - catch (const std::runtime_error& error) { submitFailed = std::strcmp(error.what(), "AIO gated transfer failure") == 0; } - }); - { - std::unique_lock lock(g_aioGateMutex); - Check(g_aioGateChanged.wait_for(lock, std::chrono::seconds(5), [] { return g_aioGateEntered; })); - } - std::int32_t state = -7; - Check(sceKernelAioPollRequest(pending, &state) == 0 && state == 2); - for (const auto operation : {sceKernelAioCancelRequest, sceKernelAioDeleteRequest}) { - state = -7; - bool refused = false; - try { operation(pending, &state); } catch (const std::runtime_error&) { refused = true; } - Check(refused && state == -7); - } - for (const auto operation : {sceKernelAioCancelRequests, sceKernelAioDeleteRequests}) { - std::int32_t ids[2]{completed, pending}; - std::int32_t states[2]{-7, -7}; - bool refused = false; - try { operation(ids, 2, states); } catch (const std::runtime_error&) { refused = true; } - Check(refused && states[0] == -7 && states[1] == -7); - } - Check(sceKernelAioPollRequest(completed, &state) == 0 && state == 3); - Check(sceKernelAioPollRequest(pending, &state) == 0 && state == 2); - std::uint32_t timeout = 1000; - Check(sceKernelAioWaitRequest(pending, &state, &timeout) == SCE_KERNEL_ERROR_ETIMEDOUT && state == 2); - std::atomic<bool> waiterDone{false}; - std::int32_t waited = -7; - std::thread waiter([&] { - Check(sceKernelAioWaitRequest(pending, &waited, nullptr) == 0); - waiterDone.store(true); - }); - const auto waiterDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); - while (AioProcessingWaitersForTest_nid_no_patch() == 0 && std::chrono::steady_clock::now() < waiterDeadline) std::this_thread::yield(); - Check(AioProcessingWaitersForTest_nid_no_patch() == 1); - const auto concurrent = SubmitEmptyAio(); - Check(!waiterDone.load() && result.state == 7 && submittedId == -7); - { - std::lock_guard lock(g_aioGateMutex); - g_aioGateReleased = true; - g_aioGateChanged.notify_all(); - } - submitter.join(); - waiter.join(); - GuestArena::GuestArenaSetHostReadHook_nid_no_patch(nullptr); - Check(waiterDone.load() && submitFailed == fail); - Check(AioProcessingWaitersForTest_nid_no_patch() == 0); - if (fail) { - Check(waited == SCE_KERNEL_ERROR_ESRCH && submittedId == -7 && result.state == 7); - } else { - Check(waited == 3 && submittedId == pending); - Check(result.state == 3 && result.return_value == SCE_KERNEL_ERROR_EBADF); - DeleteAio(pending); - } - for (const auto id : {completed, concurrent}) DeleteAio(id); -} #ifdef _WIN32 static int g_parameterReports = 0; static void CountParameterReport(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) { @@ -279,12 +73,6 @@ static void CheckBadPositionedDescriptors() { } #endif int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "identity") == 0) { CheckAioIdentity(); return 0; } - if (argc > 1 && std::strcmp(argv[1], "admission") == 0) { CheckAioAdmission(); return 0; } - if (argc > 1 && std::strcmp(argv[1], "rollback") == 0) { CheckAioRollback(); return 0; } - if (argc > 1 && std::strcmp(argv[1], "identity-exhaustion") == 0) { CheckAioIdentityExhaustion(); return 0; } - if (argc > 1 && std::strcmp(argv[1], "processing") == 0) { CheckAioProcessingOwnership(false); return 0; } - if (argc > 1 && std::strcmp(argv[1], "processing-rollback") == 0) { CheckAioProcessingOwnership(true); return 0; } #ifdef _WIN32 if (argc > 1 && std::strcmp(argv[1], "bad-descriptors") == 0) { CheckBadPositionedDescriptors(); diff --git a/core/libs/tests/GuestAioRegistry.cpp b/core/libs/tests/GuestAioRegistry.cpp new file mode 100644 index 0000000000..1b165ec7ca --- /dev/null +++ b/core/libs/tests/GuestAioRegistry.cpp @@ -0,0 +1,251 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include <array> +#include <atomic> +#include <chrono> +#include <condition_variable> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <filesystem> +#include <fstream> +#include <iterator> +#include <limits> +#include <mutex> +#include <stdexcept> +#include <string> +#include <thread> +#include <vector> +#include "tests/AioTest.hpp" + +static void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Aio check failed at line %d\n", line); + std::abort(); + } +} +#define Check(value) Require((value), __LINE__) + +static void DeleteAio(std::int32_t id) { + std::int32_t removed = -1; + Check(sceKernelAioDeleteRequest(id, &removed) == 0 && removed == 0); +} + +static std::int32_t SubmitEmptyAio() { + KernelAioResult result{-1, 0}; + KernelAioRwRequest request{0, 0, nullptr, &result, -1}; + std::int32_t id = 0; + Check(sceKernelAioSubmitReadCommands(&request, 1, 1, &id) == 0); + Check(result.state == 3 && result.return_value == SCE_KERNEL_ERROR_EBADF); + return id; +} + +static void CheckAioIdentity() { + const auto retained = SubmitEmptyAio(); + std::int32_t state = 0; + Check(sceKernelAioPollRequest(retained, &state) == 0 && state == 3); + auto previous = retained; + for (int iteration = 0; iteration < 1100; ++iteration) { + const auto id = SubmitEmptyAio(); + Check(id > previous); + previous = id; + DeleteAio(id); + Check(sceKernelAioPollRequest(retained, &state) == 0 && state == 0x10003); + } + DeleteAio(retained); + for (int iteration = 0; iteration < 1100; ++iteration) { + const auto id = SubmitEmptyAio(); + Check(id > previous); + previous = id; + DeleteAio(id); + } + Check(sceKernelAioPollRequest(retained, &state) == 0 && state == SCE_KERNEL_ERROR_ESRCH); +} + +static void CheckAioAdmission() { + const auto file = std::filesystem::path("anyps5-aio-admission-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + { std::ofstream stream(file, std::ios::binary); stream << 'Z'; Check(static_cast<bool>(stream)); } + const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Check(fd >= 0); + std::vector<std::int32_t> retained; + for (int index = 0; index < 510; ++index) retained.push_back(SubmitEmptyAio()); + std::array<KernelAioResult, 2> results{{{-7, 7}, {-7, 7}}}; + char byte = 'X'; + KernelAioRwRequest requests[2]{{0, 1, &byte, &results[0], fd}, {0, 1, &byte, &results[1], fd}}; + std::int32_t state = 0; + Check(sceKernelAioPollRequest(retained[0], &state) == 0 && state == 3); + for (const auto submit : {sceKernelAioSubmitReadCommandsMultiple, sceKernelAioSubmitWriteCommandsMultiple}) { + std::int32_t ids[2]{-7, -7}; + bool refused = false; + try { submit(requests, 2, 1, ids); } catch (const std::runtime_error&) { refused = true; } + Check(refused && ids[0] == -7 && ids[1] == -7); + Check(results[0].state == 7 && results[0].return_value == -7 && results[1].state == 7 && results[1].return_value == -7); + } + retained.push_back(SubmitEmptyAio()); + for (const auto submit : {sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands}) { + std::int32_t id = -7; + bool refused = false; + try { submit(requests, 1, 1, &id); } catch (const std::runtime_error&) { refused = true; } + Check(refused && id == -7 && results[0].state == 7 && results[0].return_value == -7); + } + Check(sceKernelAioPollRequest(retained[0], &state) == 0 && state == 0x10003); + { std::ifstream stream(file, std::ios::binary); Check(stream.get() == 'Z' && stream.get() == std::char_traits<char>::eof()); } + for (const auto id : retained) DeleteAio(id); + std::int32_t ids[2]{}; + Check(sceKernelAioSubmitReadCommandsMultiple(requests, 2, 1, ids) == 0); + Check(ids[0] != ids[1] && results[0].state == 3 && results[1].state == 3); + for (const auto id : ids) DeleteAio(id); + Check(sceKernelClose(fd) == 0 && std::filesystem::remove(file)); +} + +static int g_aioSourceCalls = 0; + +static void FailAioSource(std::uintptr_t, std::size_t) { + if (++g_aioSourceCalls % 2 == 0) throw std::runtime_error("AIO source fault injection"); +} + +static void CheckAioRollback() { + const auto file = std::filesystem::path("anyps5-aio-rollback-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + { std::ofstream stream(file, std::ios::binary); stream << "zz"; Check(static_cast<bool>(stream)); } + const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Check(fd >= 0); + std::array<KernelAioResult, 2> results{{{-7, 7}, {-7, 7}}}; + char byte = 'A'; + KernelAioRwRequest requests[2]{{0, 1, &byte, &results[0], fd}, {1, 1, &byte, &results[1], fd}}; + for (const auto submit : {sceKernelAioSubmitWriteCommands, sceKernelAioSubmitWriteCommandsMultiple}) { + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(FailAioSource); + for (int iteration = 0; iteration < 600; ++iteration) { + results = {{{-7, 7}, {-7, 7}}}; + std::int32_t ids[2]{-7, -7}; + bool refused = false; + const auto calls = g_aioSourceCalls; + try { submit(requests, 2, 1, ids); } catch (const std::runtime_error& error) { refused = std::strcmp(error.what(), "AIO source fault injection") == 0; } + Check(refused && ids[0] == -7 && ids[1] == -7); + Check(g_aioSourceCalls == calls + 2 && results[0].state == 3 && results[0].return_value == 1 && results[1].state == 7); + } + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(nullptr); + } + Check(sceKernelClose(fd) == 0); + { std::ifstream stream(file, std::ios::binary); Check(stream.get() == 'A' && stream.get() == 'z' && stream.get() == std::char_traits<char>::eof()); } + Check(std::filesystem::remove(file)); + CheckAioAdmission(); +} + +static void CheckAioIdentityExhaustion() { + const auto maximum = std::numeric_limits<std::int32_t>::max(); + AioAdvanceIdentityForTest_nid_no_patch(maximum); + std::array<KernelAioResult, 2> results{{{-7, 7}, {-7, 7}}}; + KernelAioRwRequest requests[2]{{0, 0, nullptr, &results[0], -1}, {0, 0, nullptr, &results[1], -1}}; + std::int32_t ids[2]{-7, -7}; + bool refused = false; + try { sceKernelAioSubmitReadCommandsMultiple(requests, 2, 1, ids); } catch (const std::runtime_error&) { refused = true; } + Check(refused && ids[0] == -7 && ids[1] == -7 && results[0].state == 7 && results[1].state == 7); + const auto final = SubmitEmptyAio(); + Check(final == maximum); + DeleteAio(final); + refused = false; + try { sceKernelAioSubmitReadCommands(requests, 1, 1, ids); } catch (const std::runtime_error&) { refused = true; } + Check(refused && ids[0] == -7 && results[0].state == 7); + std::int32_t state = 0; + Check(sceKernelAioPollRequest(final, &state) == 0 && state == SCE_KERNEL_ERROR_ESRCH); +} + +static std::mutex g_aioGateMutex; +static std::condition_variable g_aioGateChanged; +static bool g_aioGateEntered = false; +static bool g_aioGateReleased = false; +static bool g_aioGateFails = false; + +static void HoldAioSource(std::uintptr_t, std::size_t) { + std::unique_lock lock(g_aioGateMutex); + g_aioGateEntered = true; + g_aioGateChanged.notify_all(); + Check(g_aioGateChanged.wait_for(lock, std::chrono::seconds(5), [] { return g_aioGateReleased; })); + if (g_aioGateFails) throw std::runtime_error("AIO gated transfer failure"); +} + +static void CheckAioProcessingOwnership(bool fail) { + g_aioGateFails = fail; + const auto maximum = std::numeric_limits<std::int32_t>::max(); + AioAdvanceIdentityForTest_nid_no_patch(maximum - 2); + const auto completed = SubmitEmptyAio(); + const auto pending = completed + 1; + KernelAioResult result{-7, 7}; + char byte = 'x'; + KernelAioRwRequest request{0, 1, &byte, &result, -1}; + std::int32_t submittedId = -7; + bool submitFailed = false; + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(HoldAioSource); + std::thread submitter([&] { + try { Check(sceKernelAioSubmitWriteCommands(&request, 1, 1, &submittedId) == 0); } + catch (const std::runtime_error& error) { submitFailed = std::strcmp(error.what(), "AIO gated transfer failure") == 0; } + }); + { + std::unique_lock lock(g_aioGateMutex); + Check(g_aioGateChanged.wait_for(lock, std::chrono::seconds(5), [] { return g_aioGateEntered; })); + } + std::int32_t state = -7; + Check(sceKernelAioPollRequest(pending, &state) == 0 && state == 2); + for (const auto operation : {sceKernelAioCancelRequest, sceKernelAioDeleteRequest}) { + state = -7; + bool refused = false; + try { operation(pending, &state); } catch (const std::runtime_error&) { refused = true; } + Check(refused && state == -7); + } + for (const auto operation : {sceKernelAioCancelRequests, sceKernelAioDeleteRequests}) { + std::int32_t ids[2]{completed, pending}; + std::int32_t states[2]{-7, -7}; + bool refused = false; + try { operation(ids, 2, states); } catch (const std::runtime_error&) { refused = true; } + Check(refused && states[0] == -7 && states[1] == -7); + } + Check(sceKernelAioPollRequest(completed, &state) == 0 && state == 3); + Check(sceKernelAioPollRequest(pending, &state) == 0 && state == 2); + std::uint32_t timeout = 1000; + Check(sceKernelAioWaitRequest(pending, &state, &timeout) == SCE_KERNEL_ERROR_ETIMEDOUT && state == 2); + std::atomic<bool> waiterDone{false}; + std::int32_t waited = -7; + std::thread waiter([&] { + Check(sceKernelAioWaitRequest(pending, &waited, nullptr) == 0); + waiterDone.store(true); + }); + const auto waiterDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (AioProcessingWaitersForTest_nid_no_patch() == 0 && std::chrono::steady_clock::now() < waiterDeadline) std::this_thread::yield(); + Check(AioProcessingWaitersForTest_nid_no_patch() == 1); + const auto concurrent = SubmitEmptyAio(); + Check(!waiterDone.load() && result.state == 7 && submittedId == -7); + { + std::lock_guard lock(g_aioGateMutex); + g_aioGateReleased = true; + g_aioGateChanged.notify_all(); + } + submitter.join(); + waiter.join(); + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(nullptr); + Check(waiterDone.load() && submitFailed == fail); + Check(AioProcessingWaitersForTest_nid_no_patch() == 0); + if (fail) { + Check(waited == SCE_KERNEL_ERROR_ESRCH && submittedId == -7 && result.state == 7); + } else { + Check(waited == 3 && submittedId == pending); + Check(result.state == 3 && result.return_value == SCE_KERNEL_ERROR_EBADF); + DeleteAio(pending); + } + for (const auto id : {completed, concurrent}) DeleteAio(id); +} + +int main(int argc, char** argv) { + Check(argc == 2); + if (argc > 1 && std::strcmp(argv[1], "identity") == 0) { CheckAioIdentity(); return 0; } + if (argc > 1 && std::strcmp(argv[1], "admission") == 0) { CheckAioAdmission(); return 0; } + if (argc > 1 && std::strcmp(argv[1], "rollback") == 0) { CheckAioRollback(); return 0; } + if (argc > 1 && std::strcmp(argv[1], "identity-exhaustion") == 0) { CheckAioIdentityExhaustion(); return 0; } + if (argc > 1 && std::strcmp(argv[1], "processing") == 0) { CheckAioProcessingOwnership(false); return 0; } + if (argc > 1 && std::strcmp(argv[1], "processing-rollback") == 0) { CheckAioProcessingOwnership(true); return 0; } + Check(false); +} From f75e51c5d417080a228b6c8a5782577eba0b78eb Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Tue, 27 Oct 2026 01:30:15 -0300 Subject: [PATCH 288/702] test(libkernel): cover cancellation during entry-return teardown --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- .../tests/GuestPthreadCancelInactiveWait.cpp | 25 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index f081836d5e..efd754aa92 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -871,7 +871,7 @@ if(BUILD_TESTING) target_include_directories(guest_pthread_cancel_inactive_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_cancel_inactive_wait_tests PRIVATE libkernel libc) configure_windows_unwind(guest_pthread_cancel_inactive_wait_tests) - foreach(waitState IN ITEMS cleanup destructor) + foreach(waitState IN ITEMS cleanup destructor return) foreach(waitMode IN ITEMS untimed timed) foreach(requestApi IN ITEMS sce posix) set(inactiveWaitTest guest_pthread_cancel_inactive_wait_${waitState}_${waitMode}_${requestApi}) diff --git a/core/libs/tests/GuestPthreadCancelInactiveWait.cpp b/core/libs/tests/GuestPthreadCancelInactiveWait.cpp index ed8a0aa083..c7fddeca47 100644 --- a/core/libs/tests/GuestPthreadCancelInactiveWait.cpp +++ b/core/libs/tests/GuestPthreadCancelInactiveWait.cpp @@ -19,10 +19,10 @@ static void Require(bool value, std::source_location location = std::source_loca } template <class TPredicate> -static void Await(TPredicate predicate) { +static void Await(TPredicate predicate, std::source_location location = std::source_location::current()) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (!predicate() && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); - Require(predicate()); + Require(predicate(), location); } struct Gate { @@ -71,6 +71,7 @@ struct Context { PthreadKey key = 0; bool timed = false; bool destructorWait = false; + bool naturalReturn = false; bool ready = false; std::atomic<bool> waitReturned{false}; std::atomic<bool> returned{false}; @@ -91,7 +92,7 @@ static void Wait(Context& context) { static void APS5_VABI DestroySpecific(void* argument) { auto& context = *static_cast<Context*>(argument); if (context.destructorWait) Wait(context); - Require(context.waitReturned && context.cleanup == 1); + Require(context.waitReturned && context.cleanup == (context.naturalReturn ? 0 : 1)); ++context.destructors; } @@ -104,6 +105,10 @@ static void APS5_VABI Cleanup(void* argument) { static void* APS5_VABI Worker(void* argument) { auto& context = *static_cast<Context*>(argument); Require(scePthreadSetspecific(context.key, &context) == 0); + if (context.naturalReturn) { + context.returned = true; + return &context; + } CleanupHandler cleanup{}; __pthread_cleanup_push_imp_nid_postfix(Cleanup, &context, &cleanup); scePthreadExit(&context); @@ -111,10 +116,12 @@ static void* APS5_VABI Worker(void* argument) { return nullptr; } -static void Check(bool timed, bool destructorWait, bool posix) { +static void Check(bool timed, bool destructorWait, bool naturalReturn, bool posix) { Context context; context.timed = timed; context.destructorWait = destructorWait; + context.naturalReturn = naturalReturn; + const int cleanupCount = naturalReturn ? 0 : 1; Require(scePthreadKeyCreate(&context.key, reinterpret_cast<pthread_key_destructor_func_t>(DestroySpecific)) == 0); Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Worker, &context, nullptr) == 0); @@ -126,8 +133,8 @@ static void Check(bool timed, bool destructorWait, bool posix) { cancelReturned = true; }); Await([&] { return cancelReturned.load(); }); - Require(!context.waitReturned && !context.returned && context.destructors == 0); - Require(context.cleanup == 1); + Require(!context.waitReturned && context.returned == naturalReturn && context.destructors == 0); + Require(context.cleanup == cleanupCount); Require(thread->cancelPending.load()); cancel.join(); context.lock.gate.Release(); @@ -139,15 +146,15 @@ static void Check(bool timed, bool destructorWait, bool posix) { void* result = nullptr; Require(scePthreadJoin(thread, &result) == 0); Require(result == &context); - Require(context.waitReturned && !context.returned && context.cleanup == 1 && context.destructors == 1); + Require(context.waitReturned && context.returned == naturalReturn && context.cleanup == cleanupCount && context.destructors == 1); Require(scePthreadKeyDelete(context.key) == 0); } int main(int count, char** arguments) { Require(count == 4); const std::string_view state(arguments[1]), mode(arguments[2]), api(arguments[3]); - Require(state == "cleanup" || state == "destructor"); + Require(state == "cleanup" || state == "destructor" || state == "return"); Require(mode == "untimed" || mode == "timed"); Require(api == "sce" || api == "posix"); - Check(mode == "timed", state == "destructor", api == "posix"); + Check(mode == "timed", state != "cleanup", state == "return", api == "posix"); } From de2b96d8bdb9f36a7f59c00db34f383aaea75c5c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:30:27 +0000 Subject: [PATCH 289/702] fix(libkernel): wait for an in-flight file measurement before closing a watched descriptor --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 6 ++++++ core/libs/prx/libkernel/Equeue/Equeue.hpp | 3 +++ 2 files changed, 9 insertions(+) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 059f00798c..2419ab0dc7 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -167,6 +167,9 @@ void KernelEqueuePrivate::MeasureFiles(std::unique_lock<std::mutex>& lock) { if (files.empty()) { return; } + for (const auto& [file, owner] : files) { + m_measuring.push_back(file); + } lock.unlock(); std::vector<UnreadFile> measured; measured.reserve(files.size()); @@ -176,7 +179,9 @@ void KernelEqueuePrivate::MeasureFiles(std::unique_lock<std::mutex>& lock) { lock.lock(); for (std::size_t i = 0; i < files.size(); ++i) { *static_cast<UnreadFile*>(files[i].second.get()) = measured[i]; + m_measuring.erase(std::find(m_measuring.begin(), m_measuring.end(), files[i].first)); } + m_measured.notify_all(); } void KernelEqueuePrivate::WakePollers() { @@ -582,6 +587,7 @@ void KernelEqueuePrivate::RemoveDescriptorEvents(uintptr_t ident) { if (removed) { WakePollers(); } + m_measured.wait(lock, [&] { return std::find(m_measuring.begin(), m_measuring.end(), static_cast<int>(ident)) == m_measuring.end(); }); } KernelEqueueRef EqueuePin_nid_postfix(KernelEqueue eq) { diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 6b9194be56..98387996ea 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -6,6 +6,7 @@ #include <functional> #include <list> #include <memory> +#include <condition_variable> #include <mutex> #include <optional> #include <string> @@ -114,7 +115,9 @@ private: std::list<KernelEqueueEvent> m_events; std::vector<std::uintptr_t> m_pollers; + std::vector<int> m_measuring; std::mutex m_mutex; + std::condition_variable m_measured; TimedWait::Condition m_cond; std::string m_name; KernelEqueue m_handle; From 9b9f9b4af8480c9c4650862f9b5ebaf75b8a49d3 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:25:28 +0000 Subject: [PATCH 290/702] fix(libkernel): retire an in-flight file measurement on every exit with a guard MeasureFiles registered the descriptors it measures in m_measuring and removed them only on the normal path, so an exception between unlocking and relocking left them there and every later RemoveDescriptorEvents for one of them waited forever. Both vectors are reserved before anything is registered, and a guard owns the registration: on every exit it relocks if needed, removes the entries and notifies. --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 2419ab0dc7..a5a7097d06 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -167,21 +167,35 @@ void KernelEqueuePrivate::MeasureFiles(std::unique_lock<std::mutex>& lock) { if (files.empty()) { return; } + std::vector<UnreadFile> measured; + measured.reserve(files.size()); + m_measuring.reserve(m_measuring.size() + files.size()); + struct Measuring { + KernelEqueuePrivate& queue; + std::unique_lock<std::mutex>& lock; + const std::vector<std::pair<int, std::shared_ptr<void>>>& files; + ~Measuring() { + if (!lock.owns_lock()) { + lock.lock(); + } + for (const auto& [file, owner] : files) { + queue.m_measuring.erase(std::find(queue.m_measuring.begin(), queue.m_measuring.end(), file)); + } + queue.m_measured.notify_all(); + } + }; for (const auto& [file, owner] : files) { m_measuring.push_back(file); } + const Measuring measuring{*this, lock, files}; lock.unlock(); - std::vector<UnreadFile> measured; - measured.reserve(files.size()); for (const auto& [file, owner] : files) { measured.push_back(UnreadFileBytes(file)); } lock.lock(); for (std::size_t i = 0; i < files.size(); ++i) { *static_cast<UnreadFile*>(files[i].second.get()) = measured[i]; - m_measuring.erase(std::find(m_measuring.begin(), m_measuring.end(), files[i].first)); } - m_measured.notify_all(); } void KernelEqueuePrivate::WakePollers() { From ea4eaeae42bccd97f864181d2d2fcfe5d0359069 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:25:28 +0000 Subject: [PATCH 291/702] fix(libkernel): remove a watched descriptor's events before dup2 takes its positioned lock on Windows dup2 called EqueueDescriptorClosed(to) while holding the positioned-transfer lock of the descriptor, and the in-flight measurement it waits for needs that lock for its sceKernelLseek, so dup2 onto a watched descriptor deadlocked. The events are removed first, before the lock is taken. --- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index c1a1d1cfcd..a777f22646 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -879,13 +879,13 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (from == to) return to; if (to == 1) PreserveHostStandardOutput(); if (to == 2) PreserveHostStandardError(); + EqueueDescriptorClosed(to); auto positioned = File::LockPositionedTransfer(to); auto* const standard = HostStandardHandle_nid_no_patch(to); int duplicated = File::ReplaceSharedMemoryDescriptor_nid_no_patch(from, to); if (duplicated == -2) duplicated = WithoutParameterHandler([from, to] { return ::_dup2(from, to); }); KeepStandardHandle_nid_no_patch(to, standard); if (duplicated != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); - EqueueDescriptorClosed(to); GuestSockets::Forget(to); File::ForgetDirectoryDescriptor(to); File::DuplicateDirectoryDescriptor(from, to); From bef1faf9f56272c5c5fe90d360fb607ed64eb4b1 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:25:28 +0000 Subject: [PATCH 292/702] test(libkernel): dup2 onto a descriptor an equeue watches while it waits Next to CloseWhileWaitingOnFile: 2000 rounds of watching a regular file and replacing its descriptor with dup2 while another thread waits on the queue. --- core/libs/tests/GuestSocketEvents.cpp | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/core/libs/tests/GuestSocketEvents.cpp b/core/libs/tests/GuestSocketEvents.cpp index 59bd2bb4ad..ac1f57119f 100644 --- a/core/libs/tests/GuestSocketEvents.cpp +++ b/core/libs/tests/GuestSocketEvents.cpp @@ -149,10 +149,42 @@ static void CloseWhileWaitingOnFile() { Require(std::filesystem::remove(path)); } +static void Dup2OntoWatchedFile() { + const std::string path = "anyps5-equeue-dup2-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int created = open_nid_postfix(path.c_str(), 0x0002 | 0x0200, 0644); + Require(created >= 0); + Require(close_nid_postfix(created) == 0); + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "dup2") == SCE_OK); + std::atomic<bool> stop{false}; + std::thread waiter([eq, &stop] { + const KernelUseconds timeout = 1000; + std::array<KernelEvent, 4> events{}; + int count = 0; + while (!stop.load()) { + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + } + }); + for (int i = 0; i < 2000; ++i) { + const int watched = open_nid_postfix(path.c_str(), 0x0002, 0); + const int replacement = open_nid_postfix(path.c_str(), 0x0002, 0); + Require(watched >= 0 && replacement >= 0); + Require(sceKernelAddReadEvent(eq, watched, 0, nullptr) == SCE_OK); + Require(dup2_nid_postfix(replacement, watched) == watched); + Require(close_nid_postfix(replacement) == 0); + Require(close_nid_postfix(watched) == 0); + } + stop.store(true); + waiter.join(); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); + Require(std::filesystem::remove(path)); +} + int main() { PipeReadEvents(); RegularFileEvents(); CloseWhileWaitingOnFile(); + Dup2OntoWatchedFile(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); std::array<std::uint8_t, 16> address{16, 2, 0, 0, 127, 0, 0, 1}; From 3ad375548788e55dc6241e1be89bdb69dc4f8601 Mon Sep 17 00:00:00 2001 From: al3w0f205 <miguel.alejandro.figueroa@gmail.com> Date: Mon, 26 Oct 2026 23:52:49 -0500 Subject: [PATCH 293/702] test(libSceVrSetupDialog): cover reopen and null parameter behavior in finished dialog state --- core/libs/tests/GuestVrSetupDialog.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index 3297365257..c883f84169 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -61,6 +61,13 @@ int main() { Require(result[0] == kResultUserCanceled); Require(result[1] == -1); + Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); + param.size = 55; + Require(sceVrSetupDialogOpen(¶m) == kErrArgNull); + param.size = sizeof(param); + Require(sceVrSetupDialogOpen(¶m) == 0); + Require(sceVrSetupDialogGetStatus() == kStatusFinished); + Require(sceVrSetupDialogClose() == 0); Require(sceVrSetupDialogTerminate() == 0); Require(sceVrSetupDialogTerminate() == kErrNotInitialized); From 3123f92e7b7701247e09f9c8634d528e1a085f4d Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:51:31 +0200 Subject: [PATCH 294/702] fix(relinker): reject --lazy-binding for macOS output MacOsMachOPatcher::Patch discarded its lazyBinding argument, so a module-free `--macos --lazy-binding` conversion exited 0 and wrote the same eagerly bound Mach-O as a run without the flag. The Mach-O writer puts every import in the LC_DYLD_INFO_ONLY bind stream and leaves the lazy_bind stream empty, with no __la_symbol_ptr or stub helper, so the macOS target cannot honour the flag. The patcher now throws "macOS target does not support --lazy-binding", in the form of its --windows-diagnostics check, so such a conversion exits 2 and writes no output. Conversions with bundled modules were already refused by the guest module builder. --- core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp | 3 ++- core/relinker/relinker/tests/test_macos_output.py | 4 ++++ docs/user/USAGE.md | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index 3cb70284e7..83096e57a5 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -59,7 +59,8 @@ MacOsMachOPatcher::MacOsMachOPatcher(const std::optional<std::uint64_t> fixedBas std::vector<std::uint8_t> MacOsMachOPatcher::Patch(std::vector<std::uint8_t> sourceElf, const std::vector<Domain::ProgramHeader>& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector<Codegen::TrampolineSite>& trampolines) { if (dependencyDiagnostics) throw Domain::RelinkerException("macOS target does not support --windows-diagnostics"); - static_cast<void>(lazyBinding); + if (lazyBinding) + throw Domain::RelinkerException("macOS target does not support --lazy-binding"); Windows::WindowsLoadImage image(sourceElf, originalHeaders, true, fixedBase ? *fixedBase - Windows::LoadRva : Windows::ImageBase); if (originalPltGotVaddr != 0) diff --git a/core/relinker/relinker/tests/test_macos_output.py b/core/relinker/relinker/tests/test_macos_output.py index fd9407a851..253cfbb867 100644 --- a/core/relinker/relinker/tests/test_macos_output.py +++ b/core/relinker/relinker/tests/test_macos_output.py @@ -173,6 +173,10 @@ def check_options(relinker, directory): source.write_bytes(bytes(import_fixture())) result = subprocess.run([str(relinker), "--macos", "--windows", "--skip-sce-module", str(source), str(Path(directory) / "conflict")], capture_output=True, text=True, timeout=120) check(result.returncode != 0 and "--windows conflicts with --macos" in result.stdout + result.stderr, "--macos together with --windows was accepted") + lazy = Path(directory) / "lazy" + result = subprocess.run([str(relinker), "--macos", "--lazy-binding", "--skip-sce-module", str(source), str(lazy)], capture_output=True, text=True, timeout=120) + check(result.returncode == 2 and "macOS target does not support --lazy-binding" in result.stderr, f"--macos with --lazy-binding: exit {result.returncode}: {result.stdout}{result.stderr}") + check(not lazy.exists(), "--macos with --lazy-binding created an output") def check_runpath_lists(relinker, directory): diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index d10adeb020..ab37a47dd6 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -92,7 +92,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | | `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | | `--frame-red-zone` | Windows only: give leaf functions that keep values below RSP (the System V red zone) a frame, so an exception Windows delivers on a guest thread cannot overwrite them. Covers leaves with a `push rbp; mov rbp, rsp; push ...` prologue, leaves that only push registers, with or without a `sub` allocation after the pushes, and leaves with neither; a prologue or exit shorter than a jump takes in the instructions next to it. In leaves without a frame pointer, RSP is lowered only as far as the leaf reaches below it and their RSP operands are moved with it, in place or, when their encoding cannot hold the moved displacement, in a stub. Function bounds come from the executable's `.eh_frame`, and the relinker stops with an error when it has none. The relinker prints how many it framed and why it skipped the others, and refuses a game patch over a framed site. | -| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. The Linux executable stays position independent (`ET_DYN`) instead of being placed at `0x400000`. | +| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. The Linux executable stays position independent (`ET_DYN`) instead of being placed at `0x400000`. Windows and Linux output only. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`. From 846e2989232e60ebb4c1720c7bdb8d68b095b928 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:59:50 -0300 Subject: [PATCH 295/702] fix(agc): keep each device's resolver for deferred destruction passthrough The previous commit replaced the process-wide resolver when a device was set up after the last deferred device went, so a wrapper returned for the earlier device, which may still be alive until vkDestroyDevice, passed through to the later device's loader. The registry now keeps the resolver of every device it set up and passes each device's calls through its own one; the most recent resolver is used only for devices it never set up. Two live devices with different resolvers are still refused. --- .../Graphics/src/DeferredDestruction.cpp | 23 ++++++++++++------- .../tests/DeferredDestruction.cpp | 3 ++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp index 205b8dbf34..05da02fca1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp @@ -1,5 +1,4 @@ #include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp" -#include <atomic> #include <cstring> #include <mutex> #include <stdexcept> @@ -25,7 +24,8 @@ struct Deferred { struct Registry { std::mutex mutex; std::unordered_map<VkDevice, Deferred> devices; - std::atomic<PFN_vkGetDeviceProcAddr> resolver{nullptr}; + std::unordered_map<VkDevice, PFN_vkGetDeviceProcAddr> resolvers; + PFN_vkGetDeviceProcAddr latest = nullptr; }; Registry& registry() { @@ -33,6 +33,13 @@ Registry& registry() { return state; } +PFN_vkGetDeviceProcAddr resolverFor(VkDevice device) { + auto& state = registry(); + std::lock_guard lock(state.mutex); + const auto found = state.resolvers.find(device); + return found == state.resolvers.end() ? state.latest : found->second; +} + void destroyAll(VkDevice device, const Deferred& deferred) noexcept { for (const auto buffer : deferred.buffers) deferred.destroyBuffer(device, buffer, nullptr); for (const auto memory : deferred.memory) deferred.freeMemory(device, memory, nullptr); @@ -56,18 +63,18 @@ bool defer(VkDevice device, THandle handle, std::vector<THandle> Deferred::*list VKAPI_ATTR void VKAPI_CALL deferredDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { if (buffer == VK_NULL_HANDLE || defer(device, buffer, &Deferred::buffers)) return; - reinterpret_cast<PFN_vkDestroyBuffer>(registry().resolver.load(std::memory_order_acquire)(device, "vkDestroyBuffer"))(device, buffer, allocator); + reinterpret_cast<PFN_vkDestroyBuffer>(resolverFor(device)(device, "vkDestroyBuffer"))(device, buffer, allocator); } VKAPI_ATTR void VKAPI_CALL deferredFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { if (memory == VK_NULL_HANDLE || defer(device, memory, &Deferred::memory)) return; - reinterpret_cast<PFN_vkFreeMemory>(registry().resolver.load(std::memory_order_acquire)(device, "vkFreeMemory"))(device, memory, allocator); + reinterpret_cast<PFN_vkFreeMemory>(resolverFor(device)(device, "vkFreeMemory"))(device, memory, allocator); } VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL deferringDeviceProc(VkDevice device, const char* name) { if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&deferredDestroyBuffer); if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&deferredFreeMemory); - return registry().resolver.load(std::memory_order_acquire)(device, name); + return resolverFor(device)(device, name); } } @@ -80,10 +87,10 @@ PFN_vkGetDeviceProcAddr DeferDestructionToIdle(VkDevice device, PFN_vkGetDeviceP if (deferred.destroyBuffer == nullptr || deferred.freeMemory == nullptr) throw std::runtime_error("AGC graphics: missing Vulkan function: vkDestroyBuffer or vkFreeMemory"); auto& state = registry(); std::lock_guard lock(state.mutex); - const auto current = state.resolver.load(std::memory_order_acquire); - if (current != nullptr && current != resolver && !state.devices.empty()) throw std::runtime_error("AGC graphics: deferred destruction found two different device resolvers"); + if (!state.devices.empty() && state.latest != resolver) throw std::runtime_error("AGC graphics: deferred destruction found two different device resolvers"); if (!state.devices.emplace(device, std::move(deferred)).second) throw std::runtime_error("AGC graphics: deferred destruction was set up twice for one device"); - state.resolver.store(resolver, std::memory_order_release); + state.resolvers.insert_or_assign(device, resolver); + state.latest = resolver; return &deferringDeviceProc; } diff --git a/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp b/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp index 39c3d02ad0..c366f09d2a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp @@ -136,13 +136,14 @@ PFN_vkGetDeviceProcAddr TrySetUp(VkDevice device, PFN_vkGetDeviceProcAddr resolv void ALaterDeviceUsesItsOwnResolver() { events.clear(); const auto first = Handle<VkDevice>(0x103); - DeferDestructionToIdle(first, mockProc); + const auto firstProc = DeferDestructionToIdle(first, mockProc); DestroyDeferredObjects(first); const auto second = Handle<VkDevice>(0x104); const auto proc = TrySetUp(second, reloadedProc); Expect(proc != nullptr, "a device set up after the last one went could not use a resolver from a reloaded Vulkan loader"); if (proc == nullptr) return; Expect(ContextFor(second, proc).Function<PFN_vkQueueWaitIdle>("vkQueueWaitIdle") == &reloadedQueueWaitIdle, "a later device did not pass through to its own resolver"); + Expect(reinterpret_cast<PFN_vkQueueWaitIdle>(firstProc(first, "vkQueueWaitIdle")) == &mockQueueWaitIdle, "an earlier device's wrapper passed through to a later device's resolver"); Expect(TrySetUp(Handle<VkDevice>(0x105), mockProc) == nullptr, "two live devices with different resolvers were accepted"); DestroyDeferredObjects(second); } From 5e1b8849d2e361874ec35dd52cf9ae070f857538 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 <best.dv1996+github@gmail.com> Date: Tue, 27 Oct 2026 10:32:53 +0530 Subject: [PATCH 296/702] fix(libScePsml): answer console-measured MFSR enable flags and Context1300 size check Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- core/libs/prx/libScePsml_debug/Export.cpp | 36 ++++++++++++---- .../libs/prx/libScePsml_debug/include/Api.hpp | 5 ++- core/libs/tests/GuestPsml.cpp | 41 +++++++++++++++++++ docs/dev/debt/libScePsml_debug.md | 5 ++- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index c93e6d9ca8..315bd0e5b4 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -20,6 +20,7 @@ constexpr std::int32_t SCE_PSML_ERROR_INVALID_ARGUMENT = static_cast<std::int32_ constexpr std::int32_t SCE_PSML_ERROR_NULL_OUTPUT = static_cast<std::int32_t>(0x8A810009); constexpr std::int32_t SCE_PSML_ERROR_UNSUPPORTED_TYPE = static_cast<std::int32_t>(0x8A81000D); constexpr std::int32_t SCE_PSML_ERROR_UNSUPPORTED_CONSOLE = static_cast<std::int32_t>(0x8A810012); +constexpr std::int32_t SCE_PSML_ERROR_UNSUPPORTED_RESOLUTION = static_cast<std::int32_t>(0x8A810013); constexpr std::int32_t SCE_PSML_ERROR_SHARED_RESOURCES_BUSY = static_cast<std::int32_t>(0x8A810015); constexpr std::int32_t Mfsr2ErrorNotInitialized = static_cast<std::int32_t>(0x8A8A0001); constexpr std::int32_t Mfsr2ErrorUnsupportedConsole = static_cast<std::int32_t>(0x8A8A0012); @@ -71,6 +72,8 @@ struct MfsrObject { }; std::atomic<bool> g_mfsrInitialized{false}; +std::atomic<std::uint8_t> g_mfsrSpecularAA{0}; +std::atomic<std::uint32_t> g_mfsrFeatureFlags{0}; std::mutex g_mfsrMutex; std::unordered_map<void*, MfsrObject*> g_mfsrObjects; @@ -162,7 +165,11 @@ std::int32_t APS5_VABI scePsmlMfsrInit() { std::int32_t APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement(MfsrRequirement* requirement, const MfsrSharedResourcesParam* param) { if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; RequireMfsr(requirement != nullptr && param != nullptr, __func__, "null requirement or parameter"); - if (param->type != 0) return SCE_PSML_ERROR_UNSUPPORTED_TYPE; + if (param->type != 0) { + const bool gated = (param->type == 1 && (g_mfsrFeatureFlags.load() & 1u) != 0) || (param->type == 2 && (g_mfsrFeatureFlags.load() & 2u) != 0); + if (!gated) return SCE_PSML_ERROR_UNSUPPORTED_TYPE; + NotImplemented_nid_no_patch(__func__); + } *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrSharedBlockCount}; return 0; } @@ -211,8 +218,16 @@ std::int32_t APS5_VABI scePsmlMfsrReleaseContext(void* context) { } std::int32_t APS5_VABI scePsmlMfsrCreateContext1300(void** context, const void* param) { + if (param != nullptr) { + const auto* bytes = static_cast<const std::uint8_t*>(param); + std::uint32_t type; + std::uint16_t width, height; + std::memcpy(&type, bytes, sizeof(type)); + std::memcpy(&width, bytes + 0x18, sizeof(width)); + std::memcpy(&height, bytes + 0x1a, sizeof(height)); + if (type == 0 && (width > 3840 || height > 2160)) return SCE_PSML_ERROR_UNSUPPORTED_RESOLUTION; + } (void)context; - (void)param; if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } @@ -334,14 +349,19 @@ std::int32_t APS5_VABI scePsmlMfsr2Init() { return 0; } -int APS5_VABI scePsmlMfsrEnableSpecularAA() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePsmlMfsrEnableSpecularAA(std::uint32_t) { + g_mfsrSpecularAA = 1; + return 0; } -int APS5_VABI scePsmlMfsrEnableQuality8K() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePsmlMfsrEnableQuality8K(std::uint32_t) { + g_mfsrFeatureFlags |= 1u; + return 0; +} + +int APS5_VABI scePsmlMfsrEnableLightWeight(std::uint32_t) { + g_mfsrFeatureFlags |= 2u; + return 0; } } diff --git a/core/libs/prx/libScePsml_debug/include/Api.hpp b/core/libs/prx/libScePsml_debug/include/Api.hpp index 4a79a346d5..b856b4e055 100644 --- a/core/libs/prx/libScePsml_debug/include/Api.hpp +++ b/core/libs/prx/libScePsml_debug/include/Api.hpp @@ -127,7 +127,8 @@ std::int32_t APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, std::int32_t APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const MfsrCommandBuffer* commandBuffer, const Mfsr2DispatchParam* param); std::int32_t APS5_VABI scePsmlMfsr2CreateContext(void** context, const Mfsr2ContextParam* param); std::int32_t APS5_VABI scePsmlMfsr2Init(); -int APS5_VABI scePsmlMfsrEnableSpecularAA(); -int APS5_VABI scePsmlMfsrEnableQuality8K(); +int APS5_VABI scePsmlMfsrEnableSpecularAA(std::uint32_t); +int APS5_VABI scePsmlMfsrEnableQuality8K(std::uint32_t); +int APS5_VABI scePsmlMfsrEnableLightWeight(std::uint32_t); } diff --git a/core/libs/tests/GuestPsml.cpp b/core/libs/tests/GuestPsml.cpp index f0ec1a6ace..79ced5a38f 100644 --- a/core/libs/tests/GuestPsml.cpp +++ b/core/libs/tests/GuestPsml.cpp @@ -4,6 +4,7 @@ #include <atomic> #include <cstdint> #include <cstdlib> +#include <cstring> #include <thread> #include <vector> @@ -66,11 +67,51 @@ void SetupBeforeInit() { Require(scePsmlMfsrIsCaptureInProgress(sentinel) == kErrNotInitialized); } +void EnableFlagsReturnZero() { + Require(scePsmlMfsrEnableSpecularAA(0) == 0); + Require(scePsmlMfsrEnableSpecularAA(1) == 0); + Require(scePsmlMfsrEnableQuality8K(0) == 0); + Require(scePsmlMfsrEnableQuality8K(1) == 0); + Require(scePsmlMfsrEnableLightWeight(0) == 0); + Require(scePsmlMfsrEnableLightWeight(1) == 0); +} + +void CreateContext1300SizeCheck() { + constexpr std::int32_t unsupportedResolution = static_cast<std::int32_t>(0x8A810013); + std::array<std::uint8_t, 0x30> param{}; + const auto setType = [&](std::uint32_t value) { std::memcpy(param.data(), &value, sizeof(value)); }; + const auto setDims = [&](std::uint16_t width, std::uint16_t height) { + std::memcpy(param.data() + 0x18, &width, sizeof(width)); + std::memcpy(param.data() + 0x1a, &height, sizeof(height)); + }; + std::uint64_t contextStorage = 0; + void* const sentinel = &contextStorage; + void* context = sentinel; + + setType(0); + for (const auto [w, h] : {std::pair{3841, 2160}, {3840, 2161}, {7680, 4320}}) { + setDims(static_cast<std::uint16_t>(w), static_cast<std::uint16_t>(h)); + Require(scePsmlMfsrCreateContext1300(&context, param.data()) == unsupportedResolution); + } + for (const auto [w, h] : {std::pair{3840, 2160}, {1920, 1080}}) { + setDims(static_cast<std::uint16_t>(w), static_cast<std::uint16_t>(h)); + Require(scePsmlMfsrCreateContext1300(&context, param.data()) == kErrNotInitialized); + } + setType(1); + setDims(7680, 4320); + Require(scePsmlMfsrCreateContext1300(&context, param.data()) == kErrNotInitialized); + Require(scePsmlMfsrCreateContext1300(&context, nullptr) == kErrNotInitialized); + Require(context == sentinel); +} + void Mfsr1OnBaseConsole() { constexpr std::int32_t unsupportedConsole = static_cast<std::int32_t>(0x8A810012); + EnableFlagsReturnZero(); Require(scePsmlMfsrInit() == unsupportedConsole); Require(scePsmlMfsrInit() == unsupportedConsole); SetupBeforeInit(); + EnableFlagsReturnZero(); + CreateContext1300SizeCheck(); } void ConcurrentCreatesUninitialized() { diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index 9a520c2282..a4a829020a 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -2,11 +2,12 @@ ### Unknown function info -- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - verified from the firmware disassembly: resource type 0 is always allowed, types 1 and 2 need a console feature flag set during initialization (disallowed returns 0x8A81000D), and only one shared-resources object may exist at a time (a second create returns 0x8A810015). The context requirement reports four 2 MiB blocks regardless of its parameters, while the system calculation depends on the context configuration. The descriptor pointer at 0x28 is not read. Validation throws for null pointers, short block lists and foreign handles instead of reproducing the system error and fault behaviour. +- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - verified from the firmware disassembly: resource type 0 is always allowed, types 1 and 2 need a flag set by `scePsmlMfsrEnableQuality8K` / `scePsmlMfsrEnableLightWeight` (disallowed returns 0x8A81000D; the requirement written when the flag is present is unmeasured and throws), and only one shared-resources object may exist at a time (a second create returns 0x8A810015). The context requirement reports four 2 MiB blocks regardless of its parameters, while the system calculation depends on the context configuration. The descriptor pointer at 0x28 is not read. Validation throws for null pointers, short block lists and foreign handles instead of reproducing the system error and fault behaviour. - [scePsmlMfsr2Init and the other MFSR2 calls](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only a console without PSSR is modelled. In firmware 13.60 `libScePsml.sprx` (read from disassembly, not measured on a console), `scePsmlMfsr2Init` returns 0x8A8A0012 when `sceKernelIsTrinityMode` returns 0, and until `Init` succeeds every other MFSR2 call returns 0x8A8A0001 without touching its arguments. AnyPS5 reports 0, so `Init` throws only if that changes: the initialization after the check, the shared resource and context requirements, and the 0x800-dword size that `GetDispatchPacketsSizeInDwords` writes through its second argument are not implemented - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; they return 0x8A810001 (not initialized) and the `NotImplemented` arm is unreachable while `scePsmlMfsrInit` cannot succeed on the modelled console. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter). PPSA08330 and PPSA03016 advance a command-buffer cursor by the `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` size. Firmware 13.60 copies a 0x158-byte template per variant into a shared writer that rotates eight ring slots and emits AGC packets; the 1100/1300 arguments and console errors are unverified - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source -- [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) +- [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`). Measured on a 13.40 base console ([#11140](https://github.com/boykopovar/AnyPS5/pull/11140#issuecomment)): `scePsmlMfsrCreateContext1300` checks the resolution before the init flag — a parameter whose first dword is 0 and whose 16-bit width at 0x18 is above 3840 or height at 0x1a above 2160 returns 0x8A810013 (3841x2160, 3840x2161 and 7680x4320 all do; 3840x2160 and 1920x1080 pass through to the init check), a type-1 parameter skips the check, and everything else reaches `scePsmlMfsrCreateContext1100`'s init check. The width/height offsets are the measured ones; whether they are the same fields `MfsrContextParam` uses is unverified +- [scePsmlMfsrEnableSpecularAA, scePsmlMfsrEnableQuality8K, scePsmlMfsrEnableLightWeight](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - measured on a 13.40 base console ([#11140](https://github.com/boykopovar/AnyPS5/pull/11140#issuecomment)): all three return 0 before and after a failed init and ignore their argument, setting a byte (SpecularAA) and bits 0 and 1 of a flags word (Quality8K, LightWeight) — those bits gate `scePsmlMfsrGetSharedResourcesInitRequirement` types 1 and 2. Whether the argument is stored or the flag set unconditionally is not distinguished by the measurement; the model sets it unconditionally. `scePsmlMfsrEnableLightWeight` was not previously declared ### Functional From 369011996c80c273fd7bc71eefe7c467f90adea2 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 05:11:54 +0000 Subject: [PATCH 297/702] docs(libSceAvPlayer): note that six titles pass all four allocator callbacks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAvPlayer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index b248df63b3..c911a343f3 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -10,7 +10,7 @@ - [AvPlayer loop warnings](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) are queued when the first audio or video frame of a loop is delivered, following [KytyPS5's playback boundary tracking](https://github.com/KytyPS5/KytyPS5/blob/a6f4e8e7f456b8697d3102823d3f16400c3e121e/src/libs/avPlayer.cpp#L1038-L1043). The console's exact callback timing is unverified. Decode-ahead does not announce a loop while the caller is consuming the previous pass. - [sceAvPlayerAddSource](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown -- [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 +- [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. None of the six titles disassembled for this entry takes that path: PPSA01494, PPSA18194, PPSA21564, PPSA30839, PPSA31162 and PPSA36591 store all four allocator callbacks before each of their nine `sceAvPlayerInit` and `sceAvPlayerInitEx` calls. - [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 - [AvPlayer auto start](../../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - the auto start that `sceAvPlayerAddSource` queues for a player with auto start or without an event callback starts the player only while it is still ready: a title that starts the player itself before the auto start runs keeps its playback and the frames it holds. Not confirmed on a title. From 788a87cb8e9c9999ca15167f8acda9de7799774a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:33:02 +0000 Subject: [PATCH 298/702] feat(libkernel_sys): fail chown, lchown and fchownat with EPERM for another owner An owner or group other than the guest's (0) or the unchanged value returns EPERM after the path lookup, as FreeBSD 11's chown(2) does for a caller without the superuser privilege. That is the answer #8457 gives for fchown. A lookup error still comes first. --- core/libs/prx/libkernel_sys/Export.cpp | 14 +++++--------- core/libs/tests/GuestKernelSys.cpp | 14 +++++++++++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 6f728cbb70..122001c984 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -189,10 +189,9 @@ bool HasNonDirectoryPrefix(const std::filesystem::path& native) { return false; } -void RequireGuestOwner(const char* call, std::uint32_t uid, std::uint32_t gid) { +bool GuestOwner(std::uint32_t uid, std::uint32_t gid) { constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; - if (uid != 0 && uid != Unchanged) NotImplemented_nid_no_patch((std::string(call) + ": an owner other than the guest user").c_str()); - if (gid != 0 && gid != Unchanged) NotImplemented_nid_no_patch((std::string(call) + ": a group other than the guest group").c_str()); + return (uid == 0 || uid == Unchanged) && (gid == 0 || gid == Unchanged); } bool IsSymbolicLink(const std::filesystem::path& native) { @@ -793,23 +792,20 @@ int APS5_VABI sched_rr_get_interval_nid_postfix(int pid, KernelTimespec* interva int APS5_VABI chown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid) { FileStat status{}; if (stat_nid_postfix(path, &status) != 0) return -1; - RequireGuestOwner("chown", uid, gid); - return 0; + return GuestOwner(uid, gid) ? 0 : static_cast<int>(Fail(GuestEperm)); } int APS5_VABI lchown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid) { FileStat status{}; if (lstat_nid_postfix(path, &status) != 0) return -1; - RequireGuestOwner("lchown", uid, gid); - return 0; + return GuestOwner(uid, gid) ? 0 : static_cast<int>(Fail(GuestEperm)); } int APS5_VABI fchownat_nid_postfix(int fd, const char* path, std::uint32_t uid, std::uint32_t gid, int flag) { if (flag & ~GuestAtSymlinkNofollow) return static_cast<int>(Fail(GuestEinval)); FileStat status{}; if (fstatat_nid_postfix(fd, path, &status, flag) != 0) return -1; - RequireGuestOwner("fchownat", uid, gid); - return 0; + return GuestOwner(uid, gid) ? 0 : static_cast<int>(Fail(GuestEperm)); } int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag) { diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 40e6b87d13..683b0dd127 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -101,13 +101,21 @@ void OwnershipAndLinkTests(const std::filesystem::path& root) { Require(fchownat_nid_postfix(-100, missing.c_str(), 1000, 0, 0) == -1 && Error() == 2); Require(fchownat_nid_postfix(4321, "owned.txt", 1000, 0, 0) == -1 && Error() == 9); Require(fchownat_nid_postfix(-100, file.c_str(), 1000, 0, 0x400) == -1 && Error() == 22); - Require(ThrowsNaming([&] { chown_nid_postfix(file.c_str(), 1000, 0); }, "chown: an owner")); - Require(ThrowsNaming([&] { lchown_nid_postfix(file.c_str(), 0, 1000); }, "lchown: a group")); - Require(ThrowsNaming([&] { fchownat_nid_postfix(-100, file.c_str(), 5, Unchanged, 0); }, "fchownat: an owner")); + for (const auto call : {chown_nid_postfix, lchown_nid_postfix}) { + Require(call(file.c_str(), 1000, 0) == -1 && Error() == 1); + Require(call(file.c_str(), 0, 1000) == -1 && Error() == 1); + Require(call(file.c_str(), 1000, Unchanged) == -1 && Error() == 1); + Require(call(file.c_str(), Unchanged, 1000) == -1 && Error() == 1); + Require(call(file.c_str(), 1, 1) == -1 && Error() == 1); + } + Require(fchownat_nid_postfix(-100, file.c_str(), 5, Unchanged, 0) == -1 && Error() == 1); + Require(fchownat_nid_postfix(-100, file.c_str(), Unchanged, 5, 0x200) == -1 && Error() == 1); const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); Require(fchownat_nid_postfix(directory, "owned.txt", 0, 0, 0) == 0); Require(fchownat_nid_postfix(directory, "missing.txt", 0, 0, 0) == -1 && Error() == 2); + Require(fchownat_nid_postfix(directory, "owned.txt", 1000, 1000, 0) == -1 && Error() == 1); + Require(fchownat_nid_postfix(directory, "missing.txt", 1000, 0, 0) == -1 && Error() == 2); Require(linkat_nid_postfix(-100, file.c_str(), -100, linked.c_str(), 0) == 0); Require(std::filesystem::hard_link_count(file) == 2); Require(linkat_nid_postfix(-100, file.c_str(), -100, linked.c_str(), 0x400) == -1 && Error() == 17); From 51849e7e41159c17e6c2256f365a1c22d714b9b1 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:33:02 +0000 Subject: [PATCH 299/702] feat(libkernel_sys): implement sched_setscheduler for the calling thread FreeBSD 11's sys_sched_setscheduler checks: a null parameter is EFAULT, another process is ESRCH and an unknown policy is EINVAL. Pid 0 sets the calling thread through scePthreadSetschedparam. Values that call rejects, and the guest's own pid, whose first thread is not tracked, still throw. --- .../prx/libkernel_sys/Export.Discovery.cpp | 13 +++++++-- core/libs/prx/libkernel_sys/include/Api.hpp | 2 +- core/libs/tests/GuestKernelSys.cpp | 27 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 9c3d945fbc..7048edec38 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -2170,8 +2170,17 @@ int APS5_VABI scePthreadTimedjoin(void) { return 0; } -int APS5_VABI sched_setscheduler_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sched_setscheduler_nid_postfix(int pid, int policy, const KernelSchedParam* param) { + const auto fail = [](int error) { + *__error_nid_postfix() = error; + return -1; + }; + if (param == nullptr) return fail(14); + if (pid != 0 && pid == getpid_nid_postfix()) NotImplemented_nid_no_patch("sched_setscheduler: the first thread of the process, which its own pid selects, is not tracked"); + if (pid != 0) return fail(3); + if (policy != 1 && policy != 2 && policy != 3) return fail(22); + if (scePthreadSetschedparam(scePthreadSelf(), policy, param) != 0) + NotImplemented_nid_no_patch("sched_setscheduler: a policy and priority scePthreadSetschedparam does not accept, where FreeBSD answers EPERM or EINVAL and the console's answer is unverified"); return 0; } diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index b6d73831b3..20c5cdbbd8 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -536,7 +536,7 @@ int APS5_VABI scePthreadSingle(void); int APS5_VABI scePthreadSuspend(void); int APS5_VABI scePthreadSuspendAll(void); int APS5_VABI scePthreadTimedjoin(void); -int APS5_VABI sched_setscheduler_nid_postfix(void); +int APS5_VABI sched_setscheduler_nid_postfix(int pid, int policy, const KernelSchedParam* param); int APS5_VABI sem_setname_nid_postfix(void); int APS5_VABI set_phys_fmem_limit_nid_postfix(void); int APS5_VABI setlogin_nid_postfix(const char* name); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 683b0dd127..a64a31d421 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -332,6 +332,32 @@ void CheckSymlinkTargetRestrictions(const std::filesystem::path& root) { } } +void CheckSchedSetscheduler() { + const Pthread self = scePthreadSelf(); + int savedPolicy = 0; + KernelSchedParam saved{}; + Require(scePthreadGetschedparam(self, &savedPolicy, &saved) == 0); + KernelSchedParam param{700}; + Require(sched_setscheduler_nid_postfix(0, 1, nullptr) == -1 && Error() == 14); + Require(sched_setscheduler_nid_postfix(getpid_nid_postfix() + 1, 1, ¶m) == -1 && Error() == 3); + Require(sched_setscheduler_nid_postfix(-1, 1, ¶m) == -1 && Error() == 3); + Require(ThrowsNaming([&] { sched_setscheduler_nid_postfix(getpid_nid_postfix(), 1, ¶m); }, "first thread")); + for (const int policy : {-1, 0, 4, 7}) Require(sched_setscheduler_nid_postfix(0, policy, ¶m) == -1 && Error() == 22); + int policy = 0; + KernelSchedParam current{}; + Require(sched_setscheduler_nid_postfix(0, 1, ¶m) == 0); + Require(scePthreadGetschedparam(self, &policy, ¤t) == 0 && policy == 1 && current.sched_priority == 700); + param.sched_priority = 300; + Require(sched_setscheduler_nid_postfix(0, 3, ¶m) == 0); + Require(scePthreadGetschedparam(self, &policy, ¤t) == 0 && policy == 3 && current.sched_priority == 300); + param.sched_priority = 100; + Require(ThrowsNaming([&] { sched_setscheduler_nid_postfix(0, 1, ¶m); }, "does not accept")); + param.sched_priority = 700; + Require(ThrowsNaming([&] { sched_setscheduler_nid_postfix(0, 2, ¶m); }, "does not accept")); + Require(scePthreadGetschedparam(self, &policy, ¤t) == 0 && policy == 3 && current.sched_priority == 300); + Require(scePthreadSetschedparam(self, savedPolicy, &saved) == 0); +} + void CheckCondattrClock() { constexpr KernelClockid Realtime = 0; constexpr KernelClockid Monotonic = 4; @@ -689,6 +715,7 @@ int main() { CheckAmmWriteCommands(); CheckAmmWaitCompletion(); CheckAmmWaitConcurrentSubmits(); + CheckSchedSetscheduler(); const auto root = std::filesystem::path("anyps5-kernel-sys-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 2baf982014..a80061c243 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -27,6 +27,7 @@ - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. 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. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, 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. Console behaviour is unverified - [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 +- [sched_setscheduler](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_sched_setscheduler`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/p1003_1b.c#L198-L225) and [`ksched_setscheduler`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/ksched.c#L169-L205): a null parameter returns `EFAULT`, a pid other than 0 and the guest's own returns `ESRCH`, and a policy other than `SCHED_FIFO` (1), `SCHED_OTHER` (2) and `SCHED_RR` (3) returns `EINVAL`. Pid 0 sets the calling thread through `scePthreadSetschedparam`, with the ranges it accepts on a PS5; the values are only recorded. Values it rejects throw (FreeBSD answers `EPERM` or `EINVAL`, the console is unverified), and so does the guest's own pid, whose first thread is not tracked. Whether a title holds FreeBSD's `PRIV_SCHED_SET` is unverified - [sceKernelGetModuleInfoInternal, sceKernelGetModuleListInternal and sceKernelLoadStartModuleForSysmodule](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5. `sceKernelLoadStartModuleForSysmodule` forwards to `sceKernelLoadStartModule` - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [Console identity](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - `get_authinfo` errors and capability bits remain unverified on PS5; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp), leaving the authority ID and other capability, attribute and credential words zero. The host models a retail unit (`IsCEX` 1, `IsDevKit` 0). The system OpenPSID reuses the ordinary ID; firmware 13.42 reads a separate `machdep.openpsid_for_sys`, whose value is unknown. Null returns positive 22 per the [module report](https://github.com/boykopovar/AnyPS5/pull/10524#issuecomment-6393566914). From 8f007d234a2b08423029f75926d2c0049ccfce07 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:33:02 +0000 Subject: [PATCH 300/702] feat(libkernel_sys): implement sigreturn sigreturn applies FreeBSD 11's amd64 sys_sigreturn checks: - EFAULT for a null context; - EINVAL for unknown mc_flags, and for rflags bits outside PSL_USERCHANGE that differ from the current flags; - set_fpcontext's FPU format and ownership cases. It then restores the context through the path setcontext uses, which now lives in RestoreContext. A non-user %cs and an extended FPU state still throw. --- .../libkernel/System/include/UserContext.hpp | 2 + .../prx/libkernel/System/src/UserContext.cpp | 58 ++++++++++++-- .../prx/libkernel_sys/Export.Discovery.cpp | 5 +- core/libs/prx/libkernel_sys/include/Api.hpp | 2 +- core/libs/tests/GuestKernelSys.cpp | 79 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 6 files changed, 137 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/System/include/UserContext.hpp b/core/libs/prx/libkernel/System/include/UserContext.hpp index 485c68cf3c..b6205b21c4 100644 --- a/core/libs/prx/libkernel/System/include/UserContext.hpp +++ b/core/libs/prx/libkernel/System/include/UserContext.hpp @@ -67,4 +67,6 @@ static_assert(offsetof(UserUcontext, stackSize) == 0x340); static_assert(offsetof(UserUcontext, flags) == 0x350); static_assert(sizeof(UserUcontext) == 0x370); +extern "C" int SigreturnContext_nid_no_patch(const UserUcontext* context); + #endif diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index 0d3d7f5937..0f24cfe084 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestStacks.hpp" #include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -39,13 +40,22 @@ static_assert(offsetof(RegisterFrame, rsp) == 0x38 && offsetof(RegisterFrame, r8 static_assert(offsetof(RegisterFrame, rip) == 0x80 && offsetof(RegisterFrame, rflags) == 0x88 && offsetof(RegisterFrame, cs) == 0x90 && offsetof(RegisterFrame, ss) == 0x98); static_assert(offsetof(RegisterFrame, fxsave) == 0xa0 && sizeof(RegisterFrame) == 0x2a0); +constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr int GuestSigBlock = 1; constexpr int GuestSigSetmask = 3; constexpr std::uint64_t CarryFlag = 0x1; constexpr std::uint32_t McontextFlagMask = 0x7; +constexpr std::uint32_t McontextHasFpxstate = 0x4; +constexpr std::uint64_t UserChangeableFlags = 0x254dd5; +constexpr std::uint64_t UserPrivilegeLevel = 3; +constexpr std::uint64_t FpFormatNodev = 0x10000; constexpr std::uint64_t FpFormatXmm = 0x10002; +constexpr std::uint64_t FpOwnedNone = 0x20000; constexpr std::uint64_t FpOwnedFpu = 0x20001; +constexpr std::uint64_t FpOwnedPcb = 0x20002; +constexpr std::uint16_t InitialFpuControlWord = 0x37f; +constexpr std::uint32_t InitialMxcsr = 0x1f80; constexpr std::size_t FxsaveMxcsr = 24; constexpr std::size_t FxsaveMxcsrMask = 28; constexpr std::uint32_t DefaultMxcsrMask = 0xffbf; @@ -195,10 +205,14 @@ int APS5_VABI Aps5GetcontextFinish_nid_no_patch(UserUcontext* context, const Reg return 0; } -int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { - if (context == nullptr) return Fail(GuestEinval); +} + +namespace { + +enum class FpuSource { Context, Current, Initial }; + +[[noreturn]] void RestoreContext(const UserUcontext* context, FpuSource fpu) { const auto& machine = context->mcontext; - if (machine.len != sizeof(UserMcontext) || (machine.flags & ~McontextFlagMask) != 0) return Fail(GuestEinval); RegisterFrame frame{}; frame.rax = machine.rax; frame.rbx = machine.rbx; @@ -218,9 +232,16 @@ int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { frame.r15 = machine.r15; frame.rip = machine.rip; frame.rflags = machine.rflags; - std::memcpy(frame.fxsave, machine.fpstate, sizeof(frame.fxsave)); - std::uint32_t mxcsr = 0; - std::memcpy(&mxcsr, frame.fxsave + FxsaveMxcsr, sizeof(mxcsr)); + std::uint32_t mxcsr = InitialMxcsr; + if (fpu == FpuSource::Context) { + std::memcpy(frame.fxsave, machine.fpstate, sizeof(frame.fxsave)); + std::memcpy(&mxcsr, frame.fxsave + FxsaveMxcsr, sizeof(mxcsr)); + } else if (fpu == FpuSource::Current) { + _fxsave64(frame.fxsave); + std::memcpy(&mxcsr, frame.fxsave + FxsaveMxcsr, sizeof(mxcsr)); + } else { + std::memcpy(frame.fxsave, &InitialFpuControlWord, sizeof(InitialFpuControlWord)); + } mxcsr &= HostMxcsrMask(); std::memcpy(frame.fxsave + FxsaveMxcsr, &mxcsr, sizeof(mxcsr)); sigprocmask_nid_postfix(GuestSigSetmask, context->sigmask, nullptr); @@ -235,3 +256,28 @@ int APS5_VABI Aps5SwapcontextFinish_nid_no_patch(UserUcontext* saved, const User } } + +extern "C" { + +int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { + if (context == nullptr) return Fail(GuestEinval); + const auto& machine = context->mcontext; + if (machine.len != sizeof(UserMcontext) || (machine.flags & ~McontextFlagMask) != 0) return Fail(GuestEinval); + RestoreContext(context, FpuSource::Context); +} + +int SigreturnContext_nid_no_patch(const UserUcontext* context) { + if (context == nullptr) return Fail(GuestEfault); + const auto& machine = context->mcontext; + if ((machine.flags & ~McontextFlagMask) != 0) return Fail(GuestEinval); + if (((machine.rflags ^ __builtin_ia32_readeflags_u64()) & ~UserChangeableFlags) != 0) return Fail(GuestEinval); + if ((machine.cs & 3) != UserPrivilegeLevel) NotImplemented_nid_no_patch("sigreturn: a %cs that is not a user selector, for which FreeBSD raises SIGBUS"); + if ((machine.flags & McontextHasFpxstate) != 0) NotImplemented_nid_no_patch("sigreturn: an extended FPU state (_MC_HASFPXSTATE)"); + if (machine.fpformat == FpFormatNodev) RestoreContext(context, FpuSource::Current); + if (machine.fpformat != FpFormatXmm) return Fail(GuestEinval); + if (machine.ownedfp == FpOwnedNone) RestoreContext(context, FpuSource::Initial); + if (machine.ownedfp != FpOwnedFpu && machine.ownedfp != FpOwnedPcb) return Fail(GuestEinval); + RestoreContext(context, FpuSource::Context); +} + +} diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 7048edec38..96762b03e7 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -2199,9 +2199,8 @@ int APS5_VABI sigqueue_nid_postfix(void) { return 0; } -int APS5_VABI sigreturn_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sigreturn_nid_postfix(const UserUcontext* context) { + return SigreturnContext_nid_no_patch(context); } int APS5_VABI statfs_nid_postfix(void) { diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 20c5cdbbd8..a010be1966 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -541,7 +541,7 @@ int APS5_VABI sem_setname_nid_postfix(void); int APS5_VABI set_phys_fmem_limit_nid_postfix(void); int APS5_VABI setlogin_nid_postfix(const char* name); int APS5_VABI sigqueue_nid_postfix(void); -int APS5_VABI sigreturn_nid_postfix(void); +int APS5_VABI sigreturn_nid_postfix(const UserUcontext* context); int APS5_VABI statfs_nid_postfix(void); int APS5_VABI swapon_nid_postfix(void); int APS5_VABI symlinkat_nid_postfix(void); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a64a31d421..f1e120e172 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -21,6 +21,7 @@ #include <cstdio> #include <cstdlib> #include <cstring> +#include <xmmintrin.h> #include <filesystem> #include <fstream> #include <source_location> @@ -332,6 +333,83 @@ void CheckSymlinkTargetRestrictions(const std::filesystem::path& root) { } } +UserUcontext sigreturnContext{}; +UserUcontext sigreturnResume{}; +volatile int sigreturnPasses = 0; +volatile std::uint32_t sigreturnMxcsr = 0; + +std::uint32_t RoundingAndMasks(std::uint32_t mxcsr) { + return mxcsr & ~0x3fu; +} + +void SetContextMxcsr(UserUcontext& context, std::uint32_t mxcsr) { + std::memcpy(reinterpret_cast<std::uint8_t*>(context.mcontext.fpstate) + 24, &mxcsr, sizeof(mxcsr)); +} + +void CheckSigreturnRefusals(const UserUcontext& valid) { + UserUcontext bad = valid; + bad.mcontext.flags = 0x8; + Require(sigreturn_nid_postfix(&bad) == -1 && Error() == 22); + for (const std::uint64_t privileged : {0x200ull, 0x3000ull, 0x20000ull, 0x80000ull}) { + bad = valid; + bad.mcontext.rflags ^= privileged; + Require(sigreturn_nid_postfix(&bad) == -1 && Error() == 22); + } + bad = valid; + bad.mcontext.fpformat = 0x10001; + Require(sigreturn_nid_postfix(&bad) == -1 && Error() == 22); + bad = valid; + bad.mcontext.ownedfp = 0x20003; + Require(sigreturn_nid_postfix(&bad) == -1 && Error() == 22); + bad = valid; + bad.mcontext.cs &= ~std::uint64_t{3}; + Require(ThrowsNaming([&] { sigreturn_nid_postfix(&bad); }, "not a user selector")); + bad = valid; + bad.mcontext.flags = 0x4; + Require(ThrowsNaming([&] { sigreturn_nid_postfix(&bad); }, "extended FPU state")); +} + +void CheckSigreturn() { + Require(sigreturn_nid_postfix(nullptr) == -1 && Error() == 14); + const std::uint32_t savedMxcsr = _mm_getcsr(); + sigreturnPasses = 0; + Require(getcontext_nid_postfix(&sigreturnContext) == 0); + sigreturnMxcsr = _mm_getcsr(); + sigreturnPasses = sigreturnPasses + 1; + std::uint32_t mask[4] = {}; + Require(sigprocmask_nid_postfix(1, nullptr, mask) == 0); + const bool blocked = (mask[0] & (1u << 29)) != 0; + if (sigreturnPasses == 1) { + CheckSigreturnRefusals(sigreturnContext); + sigreturnResume = sigreturnContext; + sigreturnResume.mcontext.len = 0; + sigreturnResume.sigmask[0] |= 1u << 29; + SetContextMxcsr(sigreturnResume, 0x7f80); + Require(setcontext_nid_postfix(&sigreturnResume) == -1 && Error() == 22); + sigreturn_nid_postfix(&sigreturnResume); + Require(false); + } + if (sigreturnPasses == 2) { + Require(RoundingAndMasks(sigreturnMxcsr) == 0x7f80 && blocked); + sigreturnResume = sigreturnContext; + sigreturnResume.mcontext.fpformat = 0x10000; + SetContextMxcsr(sigreturnResume, 0x5f80); + _mm_setcsr(0x3f80); + sigreturn_nid_postfix(&sigreturnResume); + Require(false); + } + if (sigreturnPasses == 3) { + Require(RoundingAndMasks(sigreturnMxcsr) == 0x3f80 && !blocked); + sigreturnResume = sigreturnContext; + sigreturnResume.mcontext.ownedfp = 0x20000; + SetContextMxcsr(sigreturnResume, 0x5f80); + sigreturn_nid_postfix(&sigreturnResume); + Require(false); + } + Require(sigreturnPasses == 4 && RoundingAndMasks(sigreturnMxcsr) == 0x1f80 && !blocked); + _mm_setcsr(savedMxcsr); +} + void CheckSchedSetscheduler() { const Pthread self = scePthreadSelf(); int savedPolicy = 0; @@ -716,6 +794,7 @@ int main() { CheckAmmWaitCompletion(); CheckAmmWaitConcurrentSubmits(); CheckSchedSetscheduler(); + CheckSigreturn(); const auto root = std::filesystem::path("anyps5-kernel-sys-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index a80061c243..e2acd4bec1 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -28,6 +28,7 @@ - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, 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. Console behaviour is unverified - [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 - [sched_setscheduler](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_sched_setscheduler`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/p1003_1b.c#L198-L225) and [`ksched_setscheduler`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/ksched.c#L169-L205): a null parameter returns `EFAULT`, a pid other than 0 and the guest's own returns `ESRCH`, and a policy other than `SCHED_FIFO` (1), `SCHED_OTHER` (2) and `SCHED_RR` (3) returns `EINVAL`. Pid 0 sets the calling thread through `scePthreadSetschedparam`, with the ranges it accepts on a PS5; the values are only recorded. Values it rejects throw (FreeBSD answers `EPERM` or `EINVAL`, the console is unverified), and so does the guest's own pid, whose first thread is not tracked. Whether a title holds FreeBSD's `PRIV_SCHED_SET` is unverified +- [sigreturn](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) follows FreeBSD 11.4's amd64 [`sys_sigreturn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/amd64/amd64/machdep.c#L473-L576) on `setcontext`'s `ucontext_t`: a null context returns `EFAULT`, and `mc_flags` outside `_MC_FLAG_MASK` or an `rflags` differing from the current flags outside `PSL_USERCHANGE` return `EINVAL`; `mc_len` is not checked. As in [`set_fpcontext`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/amd64/amd64/machdep.c#L2349-L2370), `_MC_FPFMT_NODEV` keeps the FPU state, `_MC_FPOWNED_NONE` loads the initial one, the FPU and PCB owners load `mc_fpstate`, and other values return `EINVAL`. The rest is restored as by `setcontext`, which keeps the `%fs` and `%gs` bases FreeBSD would load. A `%cs` that is not a user selector (FreeBSD also raises `SIGBUS`) and `_MC_HASFPXSTATE` throw - [sceKernelGetModuleInfoInternal, sceKernelGetModuleListInternal and sceKernelLoadStartModuleForSysmodule](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5. `sceKernelLoadStartModuleForSysmodule` forwards to `sceKernelLoadStartModule` - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [Console identity](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - `get_authinfo` errors and capability bits remain unverified on PS5; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp), leaving the authority ID and other capability, attribute and credential words zero. The host models a retail unit (`IsCEX` 1, `IsDevKit` 0). The system OpenPSID reuses the ordinary ID; firmware 13.42 reads a separate `machdep.openpsid_for_sys`, whose value is unknown. Null returns positive 22 per the [module report](https://github.com/boykopovar/AnyPS5/pull/10524#issuecomment-6393566914). From fb3841479b45a5648747bd7707fdf9764e59c431 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:55:41 -0700 Subject: [PATCH 301/702] fix(libkernel_sys): keep the owner check of chown, lchown and fchownat unmodelled oneandonlydean's review of #8457 settled that the guest is modelled as root, and FreeBSD 11's root keeps the privilege to change a file's owner, so EPERM for another owner has no source until a console measurement shows it. chown, lchown and fchownat go back to RequireGuestOwner after the path lookup, as fchown does, and their debt entry is main's again. The EPERM assertions go without throw assertions in their place; the lookup error checks stay. --- core/libs/tests/GuestKernelSys.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index f1e120e172..30c5123ddc 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -102,20 +102,10 @@ void OwnershipAndLinkTests(const std::filesystem::path& root) { Require(fchownat_nid_postfix(-100, missing.c_str(), 1000, 0, 0) == -1 && Error() == 2); Require(fchownat_nid_postfix(4321, "owned.txt", 1000, 0, 0) == -1 && Error() == 9); Require(fchownat_nid_postfix(-100, file.c_str(), 1000, 0, 0x400) == -1 && Error() == 22); - for (const auto call : {chown_nid_postfix, lchown_nid_postfix}) { - Require(call(file.c_str(), 1000, 0) == -1 && Error() == 1); - Require(call(file.c_str(), 0, 1000) == -1 && Error() == 1); - Require(call(file.c_str(), 1000, Unchanged) == -1 && Error() == 1); - Require(call(file.c_str(), Unchanged, 1000) == -1 && Error() == 1); - Require(call(file.c_str(), 1, 1) == -1 && Error() == 1); - } - Require(fchownat_nid_postfix(-100, file.c_str(), 5, Unchanged, 0) == -1 && Error() == 1); - Require(fchownat_nid_postfix(-100, file.c_str(), Unchanged, 5, 0x200) == -1 && Error() == 1); const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); Require(fchownat_nid_postfix(directory, "owned.txt", 0, 0, 0) == 0); Require(fchownat_nid_postfix(directory, "missing.txt", 0, 0, 0) == -1 && Error() == 2); - Require(fchownat_nid_postfix(directory, "owned.txt", 1000, 1000, 0) == -1 && Error() == 1); Require(fchownat_nid_postfix(directory, "missing.txt", 1000, 0, 0) == -1 && Error() == 2); Require(linkat_nid_postfix(-100, file.c_str(), -100, linked.c_str(), 0) == 0); Require(std::filesystem::hard_link_count(file) == 2); From fb1474f03273a172b60dbd4f5b8202b96a24fbc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Mon, 26 Oct 2026 23:43:07 +0100 Subject: [PATCH 302/702] test(libkernel_sys): drop assertions that valid sched_setscheduler and sigreturn calls throw Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestKernelSys.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 30c5123ddc..70e466ce44 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -354,9 +354,6 @@ void CheckSigreturnRefusals(const UserUcontext& valid) { bad = valid; bad.mcontext.cs &= ~std::uint64_t{3}; Require(ThrowsNaming([&] { sigreturn_nid_postfix(&bad); }, "not a user selector")); - bad = valid; - bad.mcontext.flags = 0x4; - Require(ThrowsNaming([&] { sigreturn_nid_postfix(&bad); }, "extended FPU state")); } void CheckSigreturn() { @@ -409,7 +406,6 @@ void CheckSchedSetscheduler() { Require(sched_setscheduler_nid_postfix(0, 1, nullptr) == -1 && Error() == 14); Require(sched_setscheduler_nid_postfix(getpid_nid_postfix() + 1, 1, ¶m) == -1 && Error() == 3); Require(sched_setscheduler_nid_postfix(-1, 1, ¶m) == -1 && Error() == 3); - Require(ThrowsNaming([&] { sched_setscheduler_nid_postfix(getpid_nid_postfix(), 1, ¶m); }, "first thread")); for (const int policy : {-1, 0, 4, 7}) Require(sched_setscheduler_nid_postfix(0, policy, ¶m) == -1 && Error() == 22); int policy = 0; KernelSchedParam current{}; @@ -418,11 +414,6 @@ void CheckSchedSetscheduler() { param.sched_priority = 300; Require(sched_setscheduler_nid_postfix(0, 3, ¶m) == 0); Require(scePthreadGetschedparam(self, &policy, ¤t) == 0 && policy == 3 && current.sched_priority == 300); - param.sched_priority = 100; - Require(ThrowsNaming([&] { sched_setscheduler_nid_postfix(0, 1, ¶m); }, "does not accept")); - param.sched_priority = 700; - Require(ThrowsNaming([&] { sched_setscheduler_nid_postfix(0, 2, ¶m); }, "does not accept")); - Require(scePthreadGetschedparam(self, &policy, ¤t) == 0 && policy == 3 && current.sched_priority == 300); Require(scePthreadSetschedparam(self, savedPolicy, &saved) == 0); } From 7554d2095cc1ea4ae884424584ede32fc9f530e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Tue, 27 Oct 2026 06:08:49 +0100 Subject: [PATCH 303/702] fix(libkernel): keep swapcontext's finish helper externally linked Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/System/src/UserContext.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index 0f24cfe084..1445055be5 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -249,12 +249,6 @@ enum class FpuSource { Context, Current, Initial }; Aps5ContextRestore_nid_no_patch(&frame); } -int APS5_VABI Aps5SwapcontextFinish_nid_no_patch(UserUcontext* saved, const UserUcontext* next, const RegisterFrame* frame) { - if (saved == nullptr || next == nullptr) return Fail(GuestEinval); - Aps5GetcontextFinish_nid_no_patch(saved, frame); - return setcontext_nid_postfix(next); -} - } extern "C" { @@ -280,4 +274,10 @@ int SigreturnContext_nid_no_patch(const UserUcontext* context) { RestoreContext(context, FpuSource::Context); } +int APS5_VABI Aps5SwapcontextFinish_nid_no_patch(UserUcontext* saved, const UserUcontext* next, const RegisterFrame* frame) { + if (saved == nullptr || next == nullptr) return Fail(GuestEinval); + Aps5GetcontextFinish_nid_no_patch(saved, frame); + return setcontext_nid_postfix(next); +} + } From 511735840c85834cddae4b2531d497e40846c5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Tue, 27 Oct 2026 06:14:09 +0100 Subject: [PATCH 304/702] fix(libkernel_sys): keep the owner check of chown, lchown and fchownat as on main Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel_sys/Export.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 122001c984..6f728cbb70 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -189,9 +189,10 @@ bool HasNonDirectoryPrefix(const std::filesystem::path& native) { return false; } -bool GuestOwner(std::uint32_t uid, std::uint32_t gid) { +void RequireGuestOwner(const char* call, std::uint32_t uid, std::uint32_t gid) { constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; - return (uid == 0 || uid == Unchanged) && (gid == 0 || gid == Unchanged); + if (uid != 0 && uid != Unchanged) NotImplemented_nid_no_patch((std::string(call) + ": an owner other than the guest user").c_str()); + if (gid != 0 && gid != Unchanged) NotImplemented_nid_no_patch((std::string(call) + ": a group other than the guest group").c_str()); } bool IsSymbolicLink(const std::filesystem::path& native) { @@ -792,20 +793,23 @@ int APS5_VABI sched_rr_get_interval_nid_postfix(int pid, KernelTimespec* interva int APS5_VABI chown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid) { FileStat status{}; if (stat_nid_postfix(path, &status) != 0) return -1; - return GuestOwner(uid, gid) ? 0 : static_cast<int>(Fail(GuestEperm)); + RequireGuestOwner("chown", uid, gid); + return 0; } int APS5_VABI lchown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid) { FileStat status{}; if (lstat_nid_postfix(path, &status) != 0) return -1; - return GuestOwner(uid, gid) ? 0 : static_cast<int>(Fail(GuestEperm)); + RequireGuestOwner("lchown", uid, gid); + return 0; } int APS5_VABI fchownat_nid_postfix(int fd, const char* path, std::uint32_t uid, std::uint32_t gid, int flag) { if (flag & ~GuestAtSymlinkNofollow) return static_cast<int>(Fail(GuestEinval)); FileStat status{}; if (fstatat_nid_postfix(fd, path, &status, flag) != 0) return -1; - return GuestOwner(uid, gid) ? 0 : static_cast<int>(Fail(GuestEperm)); + RequireGuestOwner("fchownat", uid, gid); + return 0; } int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag) { From 72a8ab97c26f8cc2c4880e2d5a7b639776c8d4b1 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:07:19 +0000 Subject: [PATCH 305/702] test(agc): skip the MSAA storage-promotion remap on sample-order devices A device with programmable sample locations but only a 1x1 location grid (for example lavapipe) creates its multisampled color targets with storage usage from the start, so the guest sample order can be written back. MultisampleTargetStorageView then returns that already-storable image and makeStorable is a no-op, so there is no promotion allocation to retire and the generation does not change. StorageRemap asserted the opposite and failed deterministically with "MSAA storage promotion did not retain its source image". Skip StorageRemap on such devices, mirroring OrderRemap, which already runs only there. The born-storable targets lifetime stays covered by OrderRemap; StorageRemap keeps covering the promotion path on devices whose MSAA targets are not born storable (for example NVIDIA, whose 4x grid is 2x2). --- .../libSceAgcDriver/tests/execution/MultisampleRetirement.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp index 5cc437a819..f3b485fbb7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp @@ -466,6 +466,10 @@ public: void StorageRemap() { GuestBlock block; const auto target = Target(block); + if (MayReorderSamples(context, target.samples) && SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) != VK_FORMAT_UNDEFINED) { + std::puts("the device stores a multisampled color target in the guest sample order, so it is born storable with no promotion to retire; the MSAA storage-promotion remap is skipped"); + return; + } const auto resource = Resource(block); std::shared_ptr<Texture> held; VkImageView sampledView = VK_NULL_HANDLE, storageView = VK_NULL_HANDLE; From 0069118a9d038f998abb36181e2f7d4c3d112725 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Tue, 27 Oct 2026 02:31:54 -0300 Subject: [PATCH 306/702] test(agc): expect no promotion in the MSAA storage twin case on sample-order devices StorageTwin asserts that a storage access promotes the rendered target to a new image. On a device that writes the guest sample order back (MayReorderSamples with a storage format, such as lavapipe with its 1x1 sample-location grid) the target is born storable, so the storage view is the rendered image and the case fails with "MSAA storage promotion kept the rendered image", the same reason StorageRemap failed. Expect the same image there, and a new one everywhere else. --- .../libSceAgcDriver/tests/execution/MultisampleRetirement.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp index f3b485fbb7..ad2cbf182e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp @@ -534,6 +534,7 @@ public: const auto target = Target(block); auto twin = target; twin.format = VK_FORMAT_R8G8B8A8_SRGB; + const bool bornStorable = MayReorderSamples(context, target.samples) && SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) != VK_FORMAT_UNDEFINED; const auto resource = Resource(block); VkImage renderedImage = VK_NULL_HANDLE, twinImage = VK_NULL_HANDLE, promotedImage = VK_NULL_HANDLE, promotedTwinImage = VK_NULL_HANDLE; Image rendered{}, promoted{}; @@ -549,7 +550,7 @@ public: { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); promoted = ImageFor(MultisampleTargetStorageView(context, resource), &promotedImage); - Require(promoted.generation != rendered.generation, "MSAA storage promotion kept the rendered image"); + Require(bornStorable == (promoted.generation == rendered.generation), bornStorable ? "a born-storable MSAA target was promoted to a new image" : "MSAA storage promotion kept the rendered image"); const auto promotedTwin = MultisampleTargetView(context, twin); ImageFor(promotedTwin, &promotedTwinImage); Require(promotedTwinImage == promotedImage, "after storage promotion an sRGB use of the MSAA surface still views the retired image"); From 4564b15ee169946bae62c4df795cea50e71e1586 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Mon, 26 Oct 2026 22:21:17 -0300 Subject: [PATCH 307/702] test(tools): restore response output preservation coverage --- tools/tests/test_check_compile.py | 67 +++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index e40969615c..0f778a13e8 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -863,6 +863,73 @@ class CheckCompileResponseTests(CheckCompileFixture): self.assertEqual(result.returncode, 2, result.stdout + result.stderr) self.assertNotIn("Traceback", result.stderr) + def check_response_options(self, cases): + compiler = shutil.which("clang") or shutil.which("gcc") + if compiler is None: + self.skipTest("needs a GNU-compatible C compiler") + source = self.source("unit.c") + response = self.build / "flags.rsp" + output_options = self.build / "output flags.rsp" + outputs = [self.build / name for name in ("unit.o", "unit.d")] + output_options.write_text(" ".join('"' + value + '"' for value in + ("-c", source.as_posix(), "-MMD", "-MF", + outputs[1].as_posix(), "-o", outputs[0].as_posix())), encoding="utf-8") + arguments = [compiler, "@" + str(response), "@" + str(output_options)] + for content, text in cases: + source.write_text(text, encoding="utf-8") + response.write_bytes(content.encode("utf-8")) + original = subprocess.run(arguments, cwd=self.build, capture_output=True, text=True, check=False) + self.assertEqual(original.returncode, 0, original.stderr) + preserved = {path: path.read_bytes() for path in (source, response, output_options, *outputs)} + for field in ("arguments", "command"): + for jobs in ("1", "2"): + with self.subTest(content=content, field=field, jobs=jobs): + command = (subprocess.list2cmdline(arguments) if os.name == "nt" else + " ".join(shlex.quote(argument) for argument in arguments)) + self.manifest([{"directory": str(self.build), "file": str(source), + field: arguments if field == "arguments" else command}]) + result = self.tool("--jobs", jobs, "unit.c") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 translation units compile", result.stdout) + for path, data in preserved.items(): + self.assertEqual(path.read_bytes(), data, path.name) + + + def test_response_macro_options_preserve_build_outputs(self): + self.check_response_options(((' -DA=7 -DB=11', '_Static_assert(A == 7 && B == 11, "definitions");\n'), ('-DA=7\n-DB=11\n', '_Static_assert(A == 7 && B == 11, "definitions");\n'))) + + def test_response_crlf_and_quoted_options_preserve_build_outputs(self): + self.check_response_options((('-DA=7\r\n-DB=11\r\n', '_Static_assert(A == 7 && B == 11, "definitions");\n'), ('-DNAME=\\"hello\\" -DB=11', '_Static_assert(sizeof(NAME) == 6 && B == 11, "quoted definition");\n'))) + + def test_unexpanded_response_files_cannot_rewrite_build_outputs(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check compilation-fragment side effects") + source = self.source("unit.c") + outputs = [self.build / name for name in ("unit.o", "unit.d", "fragment.json")] + response = self.build / "unit flags.rsp" + response.write_text(" ".join('"' + argument + '"' for argument in + ("-c", source.as_posix(), "-MMD", "-MF", outputs[1].as_posix(), + "-MJ", outputs[2].as_posix(), "-o", outputs[0].as_posix())), encoding="utf-8") + arguments = [compiler, "@" + str(response)] + original = subprocess.run(arguments, cwd=self.build, capture_output=True, text=True) + self.assertEqual(original.returncode, 0, original.stderr) + preserved = {path: path.read_bytes() for path in [source, response, *outputs]} + for field in ("arguments", "command"): + for jobs in ("1", "2"): + with self.subTest(field=field, jobs=jobs): + command = (subprocess.list2cmdline(arguments) if os.name == "nt" else + " ".join(shlex.quote(argument) for argument in arguments)) + self.manifest([{"directory": str(self.build), "file": str(source), + field: arguments if field == "arguments" else command}]) + result = self.tool("--jobs", jobs, "unit.c") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 translation units compile", result.stdout) + self.assertNotIn("Traceback", result.stderr) + for path, content in preserved.items(): + self.assertEqual(path.read_bytes(), content) + + if __name__ == "__main__": unittest.main() From d97cdbbb7f63fbb690d194149bc5349c3f585479 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 06:20:08 +0000 Subject: [PATCH 308/702] docs(libkernel): note the sceKernelVirtualQuery fields seven titles read Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index ee3f857f6d..1d02cecc60 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -307,7 +307,7 @@ - [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`, leaving errno unchanged on success. `setpriority` fails the same way; `PRIO_PROCESS` returns 0 without storing, so `getpriority` stays 0, as PS5 firmware 13.60 did in a payload process for sets of -25, -5, 5 and 25. A valid `PRIO_PGRP` or `PRIO_USER` set throws: unverified. What a title process reads, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises, are not reported or unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `RTP_SET` throws for a real-time thread (sce attributes allow only `SCHED_FIFO`/`SCHED_RR`) and for another thread: a payload process on a PS5 (firmware 12.70) answered type 2, prio 0/15/31 on the calling thread with `EINVAL`; no accepted value or set by another thread's id measured. A non-real-time thread (POSIX attributes store `SCHED_OTHER`), unmeasured, reports 3 with 0, and `RTP_SET` accepts only that. `RTP_LOOKUP` reports the policy's type (10 or 2) and the scheduling priority, as measured, not FreeBSD's 0-31; another function fails with `ESRCH` (2 measured), a null `rtprio` with `EFAULT`, an id of no live thread with `ESRCH` (unknown ids measured). `pthread_setconcurrency` stores 0 or more, returns `EINVAL` (not errno) below 0, as libthr does; `pthread_getconcurrency` returns it, a hint. Morrowind (`mshivam019/morrowind-ps5`) imports them -- [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. +- [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. Title code agrees with this layout: of 34 calls in PPSA01494, PPSA03984, PPSA18194, PPSA21564, PPSA30839, PPSA31162 and PPSA34502, 31 pass an info size of 0x48, and the 27 with the result on the stack read 64-bit values at 0x0, 0x8 and 0x10, 32-bit values at 0x18 and 0x1c and the flag byte at 0x20 (testing 0x2, 0x10 or 0x50, or masking 0x1f); one in PPSA21564 checks the name's first byte at 0x21. - [app0.zar files and directories](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - archive directory snapshots refresh on rewind; ordering and seek cookies are host packaging behavior, not console-measured behavior. Disk entries shadow archive names under the archive component comparison. Virtual directory descriptors reject data I/O, mapping, locks, filesystem queries and metadata mutation before touching their null-device backing; ordinary host directories retain native operations. Archived files remain read-only, with duplicate descriptors sharing their cursor. `lstat`, access and other ordinary path operations on archive-only entries remain unsupported. Validation uses synthetic archives on Windows and WSL, without PS5 or title runs. - [sceKernelLoadStartModule](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) loads a system library named by its console path, `/system/common/lib/<name>.sprx` or the sandboxed `/<sandbox word>/common/lib/<name>.sprx` that `sceKernelGetFsSandboxRandomWord` names, from the host `libs/<name>.prx`, as libSceSysmodule loads modules by ID; a host library has no `module_start`, so `res` is 0. Other system directories, such as `/system/priv/lib`, are not mapped. The OpenOrbis piglet sample loads `libScePigletv2VSH.sprx` and `libScePrecompiledShaders.sprx` this way - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. From 8f2f7b89717b386362ea02fbaecc03624c28c32d Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 06:25:19 +0000 Subject: [PATCH 309/702] docs(libSceAudioOut): note which port state fields five titles read Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAudioOut.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 7946f949d4..8b4ab453e7 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -29,7 +29,7 @@ - [Legacy vibration ports](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp): PPSA99902's [public input wrapper](https://github.com/Nazky/UnleashedRecomp-Prospero/blob/2ce067a1d51bb58670c7ee4a3aed893c504da805/UnleashedRecomp/hid/driver/sdl_hid.cpp#L316) opens type 10 with 256 stereo S16 or F32 frames at 48000 Hz. Mono duplicates its channel as AudioOut2 does; wider vibration layouts throw. Native controller output uses AudioOut2's four-channel layout, or its six-channel PulseAudio mapping. Without a controller sound card the existing RMS motor transform is applied to segments of at most 256 frames, scheduled by the legacy queue's frame count and rate; drain and close remove only that port's segments. Concurrent sources combine by the maximum per motor, and the pad consumer uses this approximation only for controllers other than DualSense. This host timing and motor model is not a console measurement or waveform reproduction; legacy native output is unmeasured on hardware, and user routing retains the single-controller limitation. AudioOut2 retains its separate 80 ms peak hold. - [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the native wrapper consumes a 64-bit device ID, port type, sample count, frequency and format from argument slots 1, 2, 4, 5 and 6. Slot 3 is discarded, with the common open API index position preserved. Device enumeration and mapping to host outputs remain unimplemented; the throwing body is unchanged. -- [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator. A payload process on a PS5 (firmware 13.60), which is not the foreground application, read output 0, volume -1 and reroute counter 5 for its main ports, and 6 channels for an 8-channel port on a 5.1 system, so a title's values are unverified, including the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change. +- [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator. A payload process on a PS5 (firmware 13.60), which is not the foreground application, read output 0, volume -1 and reroute counter 5 for its main ports, and 6 channels for an 8-channel port on a 5.1 system, so a title's values are unverified, including the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change. Titles read the output (PPSA01494, PPSA21564), channels and volume (PPSA21564, volume against 0) and flags bit 0 (PPSA03984, PPSA21564, PPSA30839, PPSA31162); none reads the reroute counter. - [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - The speaker info reports type 0 with available bits `0x3`, the front left speaker at +30 degrees and the front right one at -30, and -360 for the azimuth and elevation of every other slot. On that console the type followed the HDMI device type and the available bits its channels: type 0 with the device set to TV, 1 to AV amplifier and 2 to sound bar, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) names television, AV receiver and sound bar (3 is headphones there), and available bits `0x3` for 2 channels and `0x3F` for 5.1 (centre at 0, surround pair at +110 and -110, -360 for the LFE). - [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - The front pair and the empty slots read the same in every setting; 7.1 on the AV amplifier reported 2 channels on that console's HDMI chain. The host output is always reported as a stereo television here. The console also refuses flag 1 below a per-process libkernel value whose source is unidentified; BunnyFl measured the boundary between `0x14FFFFF` and `0x1500000`, and it is not modelled here. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on these values (#4317), but which field matters has not been isolated - [sceAudioOut2InitializeEx and sceAudioOut2LoInit repeats](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - a repeated `sceAudioOut2InitializeEx` or `sceAudioOut2LoInit` returns 0, and either one after `sceAudioOut2Initialize` or `sceAudioOutInit` returns 0 too; the console was not measured for these two. On PS5 firmware 13.60 a repeated `sceAudioOut2Initialize` returns `0x80268004` and a repeated `sceAudioOutInit` `0x8026000e`, and `sceAudioOutInit` after `sceAudioOut2Initialize` still returns 0, which AnyPS5 follows. From a95ced504cd1b4c365cd3c6bcbbe5416b37c2f4b Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:35:33 +0200 Subject: [PATCH 310/702] feat(recompiler): read I/J from barycentric built-ins on MoltenVK MoltenVK exposes VK_KHR_fragment_shader_barycentric but its MSL step rejects PerVertexKHR, so the extension is disabled there and a pixel shader whose I/J VGPRs outlive their v_interp_p1/p2 pairs takes the generated barycentric stages, which accept only triangle lists without primitive restart on a device without geometry shaders. A new target flag, fragmentBarycentricBuiltins, enables the extension for its built-ins only. The host interpolation check then keeps the leftover I/J reads (all perspective or all linear, at the fragment's default position) and reads them from BaryCoordKHR or BaryCoordNoPerspKHR, while the interpolated attributes stay hardware varyings. Raw vertex parameters and I/J at an offset or a sample position still take the generated stages. Apple GPUs return unnormalized perspective weights unless the fragment function reads [[position]], so each perspective read keeps a neutral use of gl_FragCoord.w, loaded as a float. Devices with the full extension are unaffected; the request serialization moves to version 45. Co-authored-by: yripper <hello@joaquinsoza.dev> --- .../Execution/src/ShaderDeviceProfile.cpp | 1 + .../Execution/src/VulkanDevice.cpp | 9 ++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 12 +++++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 30 ++++++++++--- .../execution/PixelBarycentricEmulation.cpp | 14 +++++- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 ++- .../IrMetadata/ProgramMetadata.hpp | 1 + .../Optimization/HostInterpolationChecker.hpp | 2 +- .../src/HostInterpolationChecker.cpp | 12 ++++-- .../Optimization/src/ShaderInfoCollector.cpp | 1 + core/shader/recompiler/Recompiler.cpp | 3 +- core/shader/recompiler/Recompiler.hpp | 1 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 12 ++++++ .../tests/RequestSerializerTests.cpp | 43 ++++++++++++------- docs/dev/debt/shader.md | 1 + 16 files changed, 115 insertions(+), 34 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 660a9c007c..f9b21ce96a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -71,6 +71,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta checkFeature(spv::CapabilityDrawParameters, drawParameters != nullptr && drawParameters->shaderDrawParameters == VK_TRUE, "shaderDrawParameters"); checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock"); Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics"); + Require(!target.fragmentBarycentricBuiltins || barycentricEnabled, "shader device profile exposes disabled barycentric built-ins"); Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders"); Require(!target.nonConstantImageOffsets || (maintenance8 != nullptr && maintenance8->maintenance8 == VK_TRUE), "shader device profile exposes disabled maintenance8"); Require(!target.tessellation || core.tessellationShader == VK_TRUE, "shader device profile exposes disabled tessellation"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 87483bee96..414ac25241 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -383,6 +383,7 @@ struct VulkanDevice::State { bool fragmentShaderBarycentric = false; bool shaderTerminateInvocation = false; bool fusedExactArithmetic = false; + bool fragmentBarycentricBuiltins = false; bool geometryShader = false; bool largePoints = false; bool sampleRateShading = false; @@ -1338,6 +1339,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &barycentricFeatures}; state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features); state->fragmentShaderBarycentric = barycentricFeatures.fragmentShaderBarycentric == VK_TRUE; + state->fragmentBarycentricBuiltins = state->fragmentShaderBarycentric; if (state->fragmentShaderBarycentric && hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; @@ -1392,7 +1394,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : deviceExtensions.push_back(VK_EXT_HDR_METADATA_EXTENSION_NAME); state->hdrMetadata = true; } - if (state->fragmentShaderBarycentric) { + if (state->fragmentBarycentricBuiltins) { deviceExtensions.push_back(VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityFragmentBarycentricKHR); state->spirvExtensions.push_back("SPV_KHR_fragment_shader_barycentric"); @@ -1929,7 +1931,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : deviceInfo.pNext = &coverageReductionFeatures; } byteFeatures.pNext = const_cast<void*>(deviceInfo.pNext); - if (state->fragmentShaderBarycentric) { + if (state->fragmentBarycentricBuiltins) { barycentricFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &barycentricFeatures; } @@ -4025,6 +4027,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { const auto& limits = state->properties.limits; ShaderRecompiler::SpirvTarget target{VK_API_VERSION_1_1, state->meshShader ? 0x00010400u : 0x00010300u, state->subgroup.subgroupSize, ShaderRecompiler::BdaAbi::Version, state->capabilities, state->spirvExtensions, false, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize, {}, {}}; target.fragmentShaderBarycentricEnabled = state->fragmentShaderBarycentric; + target.fragmentBarycentricBuiltins = state->fragmentBarycentricBuiltins; target.nonConstantImageOffsets = state->maintenance8; target.narrowSubgroupClock = state->narrowSubgroupClock; target.float64DenormPreserve = state->float64DenormPreserve; @@ -4111,7 +4114,7 @@ Graphics::Context VulkanDevice::buildContext() const { state->depthRangeUnrestricted, true, state->subgroup, - state->fragmentShaderBarycentric, + state->fragmentBarycentricBuiltins, state->samplerAnisotropy, state->textureCompressionBC, state->detiler.get(), diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d767e03481..3f3c61aa90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8448,7 +8448,7 @@ void barycentricEmulationTests() { for (std::uint32_t location = 0; location < 11u; ++location) crowded.push_back({location, location, false, true}); expectFailure([&] { static_cast<void>(LayoutBarycentricEmulation(crowded, {true, false, false})); }, "exceed 32 locations"); - const auto compile = [&](std::span<const std::uint32_t> code, bool barycentric, std::uint32_t secondInput = 0xffffffffu) { + const auto compile = [&](std::span<const std::uint32_t> code, bool barycentric, std::uint32_t secondInput = 0xffffffffu, bool builtins = false) { auto queue = makeState(); queue.context[0x1b3] = 0x2u; queue.context[0x1b4] = 0x2u; @@ -8463,6 +8463,7 @@ void barycentricEmulationTests() { request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; request.target.fragmentShaderBarycentricEnabled = barycentric; + request.target.fragmentBarycentricBuiltins = builtins; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return Recompile(request); @@ -8510,6 +8511,15 @@ void barycentricEmulationTests() { static constexpr std::array<std::uint32_t, 6> sharedSlot{0xc8080000u, 0xc8090001u, 0xc80e0402u, 0xf800180fu, 0x03030202u, 0xbf810000u}; const auto shared = compile(sharedSlot, false, 0x400u); Require(shared.barycentricEmulation.active && !barycentricCapability(shared), "inputs sharing a slot with different interpolation did not emulate per-vertex parameters"); + static constexpr std::array<std::uint32_t, 5> interpolatesAndReadsIj{0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020100u, 0xbf810000u}; + for (const auto& code : {std::span<const std::uint32_t>(readsIj), std::span<const std::uint32_t>(interpolatesAndReadsIj)}) { + const auto builtin = compile(code, false, 0xffffffffu, true); + const auto builtinDecorations = decorations(builtin, spv::DecorationBuiltIn); + Require(!builtin.barycentricEmulation.active && barycentricCapability(builtin) && std::find(builtinDecorations.begin(), builtinDecorations.end(), static_cast<std::uint32_t>(spv::BuiltInBaryCoordKHR)) != builtinDecorations.end() && decorations(builtin, spv::DecorationPerVertexKHR).empty(), "reading the I/J VGPRs with barycentric built-ins did not use BaryCoordKHR"); + Require(std::ranges::none_of(builtin.fragmentParameters, [](const FragmentParameter& parameter) { return parameter.perVertex; }), "barycentric built-ins made a parameter per-vertex"); + } + Require(compile(interpolatesAndReadsIj, false).barycentricEmulation.active, "reading the I/J VGPRs next to v_interp_p1/p2 without the extension did not emulate barycentrics"); + Require(compile(readsVertex, false, 0xffffffffu, true).barycentricEmulation.active, "v_interp_mov of P10 with only barycentric built-ins did not emulate per-vertex parameters"); fragment = compile(readsVertex, false); Require(fragment.barycentricEmulation.active && !fragment.barycentricEmulation.smooth && fragment.fragmentParameters.size() == 1u && fragment.fragmentParameters[0].perVertex, "v_interp_mov of P10 did not emulate per-vertex parameters"); Require(decorations(fragment, spv::DecorationLocation) == std::vector<std::uint32_t>{0u} && decorations(fragment, spv::DecorationPerVertexKHR).empty() && !decorations(fragment, spv::DecorationFlat).empty() && !barycentricCapability(fragment), "the emulated per-vertex parameter was not a flat array at the first free location"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a47dc40213..3a3660696c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5740,7 +5740,10 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQS8AAAA=", "new requests did not use serialization version 47"); + require(requestPrefix(encoded, 8u) == "NVNQQTAAAAA=", "new requests did not use serialization version 48"); + auto withBarycentric = minimal; + withBarycentric.target.fragmentBarycentricBuiltins = true; + require(serializer.Deserialize(serializer.Serialize(withBarycentric)).request.target.fragmentBarycentricBuiltins && !serializer.Deserialize(encoded).request.target.fragmentBarycentricBuiltins, "barycentric built-ins were lost in serialization"); const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -5858,7 +5861,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQTAAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQTEAAAA="}) { expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5954,6 +5957,7 @@ void verifyVertexInfoVersion19() { version20Vertex.pop_back(); version20Vertex.pop_back(); version20Vertex.pop_back(); + version20Vertex.pop_back(); version20Vertex[4] = 20u; const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); @@ -5967,6 +5971,7 @@ void verifyVertexInfoVersion19() { legacy.pop_back(); legacy.pop_back(); legacy.pop_back(); + legacy.pop_back(); legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); for (const auto version : {16u, 17u, 18u, 19u}) { legacy[4] = static_cast<std::uint8_t>(version); @@ -6038,12 +6043,13 @@ void verifyVertexInfoVersion19() { orderedVersion34.pop_back(); orderedVersion34.pop_back(); orderedVersion34.pop_back(); + orderedVersion34.pop_back(); orderedVersion34.erase(orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset - 1u), orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)); orderedVersion34[4] = 34u; const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); auto orderedVersion30 = version43Bytes(orderedBytes); - orderedVersion30.resize(orderedVersion30.size() - 4u); + orderedVersion30.resize(orderedVersion30.size() - 1u - sizeof(std::uint32_t)); orderedVersion30.erase(orderedVersion30.begin() + static_cast<std::ptrdiff_t>(blendOffset + 4u), orderedVersion30.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)); orderedVersion30[4] = 30u; const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); @@ -6052,6 +6058,7 @@ void verifyVertexInfoVersion19() { orderedSpanned.target.spannedWaves = true; auto orderedVersion32 = version43Bytes(decodeBase64(serializer.Serialize(orderedSpanned))); orderedVersion32.pop_back(); + orderedVersion32.pop_back(); require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); orderedVersion32.pop_back(); orderedVersion32.pop_back(); @@ -6076,6 +6083,7 @@ void verifyVertexInfoVersion19() { version30.pop_back(); version30.pop_back(); version30.pop_back(); + version30.pop_back(); version30.erase(version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version30[4] = 30u; const auto replay30 = serializer.Deserialize(encodeBase64(version30)); @@ -6084,6 +6092,7 @@ void verifyVertexInfoVersion19() { spanned.target.spannedWaves = true; auto version32 = version43Bytes(decodeBase64(serializer.Serialize(spanned))); version32.pop_back(); + version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -6096,9 +6105,11 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.pop_back(); version29.pop_back(); + version29.pop_back(); version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); auto version36 = version43Bytes(sampledBytes); version36.pop_back(); + version36.pop_back(); version36.erase(version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -6111,6 +6122,7 @@ void verifyVertexInfoVersion19() { version28.pop_back(); version28.pop_back(); version28.pop_back(); + version28.pop_back(); version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); @@ -6120,6 +6132,7 @@ void verifyVertexInfoVersion19() { version27.pop_back(); version27.pop_back(); version27.pop_back(); + version27.pop_back(); version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); @@ -6129,6 +6142,7 @@ void verifyVertexInfoVersion19() { legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); @@ -6179,13 +6193,14 @@ void verifyDepthRemapRequests() { const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); constexpr std::size_t pointTailBytes = 13u; - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 13u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 13 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 6 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 6 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag, the pixel function LDS flag and the pixel function storage size of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 14u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 14 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 7 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 7 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag, the pixel function LDS flag, the pixel function storage size and the barycentric built-in flag of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; legacyBytes.resize(legacyBytes.size() - sizeof(std::uint32_t)); legacyBytes.pop_back(); legacyBytes.pop_back(); legacyBytes.pop_back(); + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); @@ -6197,6 +6212,7 @@ void verifyDepthRemapRequests() { version35.pop_back(); version35.pop_back(); version35.pop_back(); + version35.pop_back(); version35[4] = 35u; version35.resize(version35.size() - 1u - pointTailBytes); const auto replay35 = serializer.Deserialize(encodeBase64(version35)); @@ -6244,8 +6260,8 @@ void verifyMergedHullConfiguration() { require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); - require(legacy.size() > 54u + pointTailBytes && legacy[4] == 47u, "a merged hull request did not use serialization version 47"); - legacy.resize(legacy.size() - sizeof(std::uint32_t) - 2u - pointTailBytes); + require(legacy.size() > 55u + pointTailBytes && legacy[4] == 48u, "a merged hull request did not use serialization version 48"); + legacy.resize(legacy.size() - sizeof(std::uint32_t) - 3u - pointTailBytes); legacy.pop_back(); require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); @@ -6357,7 +6373,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 && !request.target.registrationModules && !request.target.float16DenormPreserve, "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 && !request.target.float16DenormPreserve && !request.target.fragmentBarycentricBuiltins, "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"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp index 6eb4c60d13..60254b35ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -82,10 +82,11 @@ std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t st return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; } -ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span<const std::uint32_t> pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE, bool flat = false, VkProvokingVertexModeEXT provoking = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, bool strip = false) { +ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span<const std::uint32_t> pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE, bool flat = false, VkProvokingVertexModeEXT provoking = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, bool strip = false, bool builtins = false) { Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; + target.fragmentBarycentricBuiltins = builtins; static const auto trianglePositions = ClipPositions(Emulation::Geometry); static const auto rectanglePositions = ClipPositions(Emulation::RectList); static const auto stripPositions = StripPositions(); @@ -429,6 +430,17 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (device->Target().fragmentBarycentricBuiltins) { + const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); + const auto linear = PixelInputBit(PixelInput::LinearCenter); + auto builtin = Draw(*device, Emulation::Tessellation, BarycentricPixelCode, perspective, VK_CULL_MODE_NONE, false, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, false, true); + Require(!builtin.barycentricEmulation.active, "reading PERSP_CENTER I/J with barycentric built-ins needed generated stages"); + const auto builtinSmooth = CheckBarycentrics(Emulation::Tessellation, "PERSP_CENTER I/J from BaryCoordKHR", true); + builtin = Draw(*device, Emulation::Tessellation, BarycentricPixelCode, linear, VK_CULL_MODE_NONE, false, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, false, true); + Require(!builtin.barycentricEmulation.active, "reading LINEAR_CENTER I/J with barycentric built-ins needed generated stages"); + const auto builtinLinear = CheckBarycentrics(Emulation::Tessellation, "LINEAR_CENTER I/J from BaryCoordNoPerspKHR", false); + std::printf("barycentric built-in tests passed: %u perspective and %u linear I/J pixels\n", builtinSmooth, builtinLinear); + } const bool geometry = device->GeometryLimits().has_value(); const bool tessellation = device->Target().tessellation.has_value(); if (!geometry && !tessellation) { diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index d6aca5699d..5d8cbad148 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -321,6 +321,7 @@ private: append(key, value.nonConstantImageOffsets); append(key, value.srgbDecodeFormats); append(key, value.narrowSubgroupClock); + append(key, value.fragmentBarycentricBuiltins); append(key, value.float64DenormPreserve); append(key, value.depthRangeRemap); append(key, value.registrationModules); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index bab0068b52..f68b0b021e 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -853,7 +853,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(47u); + writer.WriteU32(48u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -894,6 +894,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteBool(request.target.fusedExactArithmetic); writer.WriteBool(request.target.largePixelFunctionLdsBuffer); writer.WriteU32(request.target.pixelFunctionStorageDwords); + writer.WriteBool(request.target.fragmentBarycentricBuiltins); return base64Encode(buffer); } @@ -902,7 +903,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 > 47u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 48u) 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); @@ -961,6 +962,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.request.target.largePixelFunctionLdsBuffer = reader.ReadBool(); result.request.target.pixelFunctionStorageDwords = reader.ReadU32(); } + if (version >= 48u) result.request.target.fragmentBarycentricBuiltins = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp index ae85d35ec0..eee7578d20 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp @@ -22,6 +22,7 @@ struct IrProgramMetadata { std::uint32_t pixelVertexOrderInputs = 0; bool pixelIteratesSamples = false; bool barycentricEmulation = false; + bool barycentricBuiltinsUnnormalized = false; bool invalidatesCaches = false; std::vector<IrValue*> dynamicReads; bool shaderInfoComplete = false; diff --git a/core/shader/recompiler/Optimization/include/Optimization/HostInterpolationChecker.hpp b/core/shader/recompiler/Optimization/include/Optimization/HostInterpolationChecker.hpp index a909f6440d..3180bd20bb 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/HostInterpolationChecker.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/HostInterpolationChecker.hpp @@ -8,7 +8,7 @@ namespace ShaderRecompiler { class HostInterpolationChecker { public: - [[nodiscard]] bool Lower(IrProgram& program, const ShaderPixelInputInfo& pixel) const; + [[nodiscard]] bool Lower(IrProgram& program, const ShaderPixelInputInfo& pixel, bool barycentricBuiltins) const; }; } diff --git a/core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp b/core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp index db4de9830a..f50ffc7eb3 100644 --- a/core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp +++ b/core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp @@ -106,7 +106,7 @@ bool sharesSlots(const IrProgram& program, const ShaderPixelInputInfo& pixel) { } -bool HostInterpolationChecker::Lower(IrProgram& program, const ShaderPixelInputInfo& pixel) const { +bool HostInterpolationChecker::Lower(IrProgram& program, const ShaderPixelInputInfo& pixel, bool barycentricBuiltins) const { std::vector<IrValue*> selects; for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { @@ -130,12 +130,18 @@ bool HostInterpolationChecker::Lower(IrProgram& program, const ShaderPixelInputI } DeadCodeEliminator{}.Eliminate(program); if (MaskedSelectEliminator{}.Eliminate(program).removedSelects != 0u) DeadCodeEliminator{}.Eliminate(program); + bool smooth = false; + bool noPerspective = false; for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { - if (isBarycentric(*inst) || inst->Opcode() == IrOpcode::InterpolateHostP1) return false; + if (inst->Opcode() == IrOpcode::InterpolateHostP1) return false; + if (!isBarycentric(*inst)) continue; + const auto kind = static_cast<StageInputKind>(inst->Argument(0)->Resolve()->ImmediateU32()); + if (!barycentricBuiltins || BarycentricInputOf(kind) != kind) return false; + (kind == StageInputKind::BaryCoordSmooth ? smooth : noPerspective) = true; } } - return true; + return !(smooth && noPerspective); } } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 472ade83d6..d93f1bbf58 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -431,6 +431,7 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { info.smoothOffsetBarycentrics = true; AddInput(info, StageInputKind::BaryCoordSmooth, 0, 3, "gl_BaryCoordKHR"); if (kind == StageInputKind::BaryCoordSmoothSample && program.Metadata().pixelIteratesSamples) AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID"); + if (program.Metadata().barycentricBuiltinsUnnormalized) AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); break; case StageInputKind::BaryCoordNoPerspective: case StageInputKind::BaryCoordNoPerspectiveCenter: diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 2165ada5db..10d5661132 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -329,7 +329,8 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat } }; simplify(); - if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled && !HostInterpolationChecker{}.Lower(program, *inputInfo.pixel)) { + if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled) program.Metadata().barycentricBuiltinsUnnormalized = request.target.fragmentBarycentricBuiltins; + if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled && !HostInterpolationChecker{}.Lower(program, *inputInfo.pixel, request.target.fragmentBarycentricBuiltins)) { translateOptions.fragmentShaderBarycentricEnabled = true; program = translator.Translate(decoded, cfg, translateOptions); program.Metadata().barycentricEmulation = true; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5dd5170501..51a0b52768 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -331,6 +331,7 @@ struct SpirvTarget { bool largePixelFunctionLdsBuffer = false; std::uint32_t pixelFunctionStorageDwords = 4096u; bool deviceScopeLdsMemoryBarriers = false; + bool fragmentBarycentricBuiltins = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index f651c6f2e3..97ea701cc0 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -280,6 +280,18 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer, variable, ConstantU32(state, component + 1u)); state.module.AddFunction(spv::OpLoad, TypeF32(state), value, pointer); + if (kind == StageInputKind::BaryCoordSmooth && state.program.Metadata().barycentricBuiltinsUnnormalized) { + const auto fragCoordW = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeF32(state), fragCoordW, EmitBuiltinU32(state, StageInputKind::FragCoord, 3u)); + const auto magnitude = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), magnitude, GlslStd450(state), GLSLstd450FAbs, fragCoordW); + const auto bound = state.module.AllocateId(); + state.module.AddFunction(spv::OpFAdd, TypeF32(state), bound, value, magnitude); + const auto kept = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), kept, GlslStd450(state), GLSLstd450FMin, value, bound); + state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, kept); + return bits; + } state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); return bits; } diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 95e13fc7e6..ee4d5d7d72 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -298,7 +298,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 47u; + upgraded[4] = 48u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); @@ -307,6 +307,7 @@ int main() { upgraded.push_back(0u); upgraded.push_back(0u); upgraded.insert(upgraded.end(), {0u, 0x10u, 0u, 0u}); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -341,7 +342,7 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 47u; + layered[4] = 48u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); @@ -350,6 +351,7 @@ int main() { layered.push_back(0u); layered.push_back(0u); layered.insert(layered.end(), {0u, 0x10u, 0u, 0u}); + layered.push_back(0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -370,7 +372,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 47u; + bytes[4] = 48u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); @@ -378,6 +380,7 @@ int main() { bytes.push_back(0u); bytes.push_back(0u); bytes.insert(bytes.end(), {0u, 0x10u, 0u, 0u}); + bytes.push_back(0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -503,7 +506,7 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false, false); auto upgraded = main38; - upgraded[4] = 47u; + upgraded[4] = 48u; if (fixture.pixel) { const auto tail = PixelTailDefaults(true, true); upgraded.insert(upgraded.begin() + 317u, tail.begin(), tail.end()); @@ -514,6 +517,7 @@ int main() { upgraded.push_back(0u); upgraded.push_back(0u); upgraded.insert(upgraded.end(), {0u, 0x10u, 0u, 0u}); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -529,9 +533,10 @@ int main() { pointed.context.vertex->pointSize = 3.0f; pointed.context.vertex->pointSizeMin = 1.0f; pointed.context.vertex->pointSizeMax = 8.0f; - auto version47 = DecodeBase64(serializer.Serialize(pointed)); - Require(version47.size() > 15u && version47[4] == 47u); - auto version46 = version47; + auto version48 = DecodeBase64(serializer.Serialize(pointed)); + Require(version48.size() > 16u && version48[4] == 48u); + auto version46 = version48; + version46.pop_back(); version46[4] = 46u; const auto replay46 = serializer.Deserialize(EncodeBase64(version46)); Require(replay46.request.graphics->tessellation->mergedHull == merged && replay46.request.context.vertex->pointList && replay46.request.context.vertex->pointSize == 3.0f && replay46.request.context.vertex->pointSizeMin == 1.0f && replay46.request.context.vertex->pointSizeMax == 8.0f); @@ -546,7 +551,7 @@ int main() { version43.pop_back(); version43[4] = 43u; const auto replay43 = serializer.Deserialize(EncodeBase64(version43)); - Require(serializer.Serialize(replay43.request) == EncodeBase64(version47)); + Require(serializer.Serialize(replay43.request) == EncodeBase64(version48)); Require(replay43.request.graphics->tessellation->mergedHull == merged && replay43.request.context.vertex->pointList && replay43.request.context.vertex->pointSize == 3.0f && replay43.request.context.vertex->pointSizeMin == 1.0f && replay43.request.context.vertex->pointSizeMax == 8.0f); auto version42 = version43; version42.resize(version42.size() - 13u); @@ -573,7 +578,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 47u; + main25[4] = 48u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -584,6 +589,7 @@ int main() { main25.push_back(0u); main25.push_back(0u); main25.insert(main25.end(), {0u, 0x10u, 0u, 0u}); + main25.push_back(0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -601,17 +607,18 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve && replay.request.target.pixelFunctionStorageDwords == request.target.pixelFunctionStorageDwords); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 47u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 48u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; version40.resize(version40.size() - sizeof(std::uint32_t)); version40.pop_back(); version40.pop_back(); version40.pop_back(); + version40.pop_back(); version40[4] = 40u; const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 48u; + future[4] = 49u; bool futureRejected = false; try { static_cast<void>(serializer.Deserialize(EncodeBase64(future))); @@ -660,6 +667,7 @@ int main() { version42.resize(version42.size() - 4u); version42.pop_back(); version42.pop_back(); + version42.pop_back(); version42[4] = 42u; const auto replay42 = serializer.Deserialize(EncodeBase64(version42)); Require(replay42.request.context.pixel.has_value() && replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && replay42.request.context.pixel->targetMerge == ShaderRecompiler::TargetMerge{}); @@ -689,6 +697,7 @@ int main() { version35[4] = 35u; version35.pop_back(); version35.pop_back(); + version35.pop_back(); const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); Require(replay35.request.target.spannedWaves == spanned && !replay35.request.target.float32FlushToZero); } @@ -707,6 +716,7 @@ int main() { auto pointBytes = DecodeBase64(pointEncoded); pointBytes.pop_back(); pointBytes.pop_back(); + pointBytes.pop_back(); bool pointRejected = false; try { static_cast<void>(serializer.Deserialize(EncodeBase64(pointBytes))); @@ -726,7 +736,7 @@ int main() { for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { pointRequest.target.spannedWaves = spanned; auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); - legacy.resize(legacy.size() - (version >= 36u ? 20u : version >= 35u ? 21u : 26u)); + legacy.resize(legacy.size() - (version >= 36u ? 21u : version >= 35u ? 22u : 27u)); legacy[4] = version; const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); Require(replayLegacy.request.target.spannedWaves == spanned); @@ -738,7 +748,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 47u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 20u : version >= 35u ? 21u : 26u)); + Require(upgradedLegacy[4] == 48u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 21u : version >= 35u ? 22u : 27u)); } } @@ -752,6 +762,7 @@ int main() { legacy43.resize(legacy43.size() - 4u); legacy43.pop_back(); legacy43.pop_back(); + legacy43.pop_back(); legacy43[4] = 43u; const auto pointReplay43 = serializer.Deserialize(EncodeBase64(legacy43)); Require(pointReplay43.request.context.pixel->sampleOrder == pixel.sampleOrder && pointReplay43.request.context.vertex->pointList && pointReplay43.request.context.vertex->pointSize == 4.0f); @@ -764,7 +775,7 @@ int main() { pixel.sampleOrder = {}; pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy34.resize(legacy34.size() - 26u); + legacy34.resize(legacy34.size() - 27u); const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto legacyPixelTail = static_cast<std::ptrdiff_t>(1u + sizeof(pixel.sampleOrder)); Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); @@ -779,7 +790,8 @@ int main() { request.target.fusedExactArithmetic = fused; request.target.largePixelFunctionLdsBuffer = false; auto bytes = DecodeBase64(serializer.Serialize(request)); - Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 6u] == (fused ? 1u : 0u) && bytes[bytes.size() - 5u] == 0u); + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 7u] == (fused ? 1u : 0u) && bytes[bytes.size() - 6u] == 0u); + bytes.pop_back(); const auto fusedMerge = std::search(bytes.begin(), bytes.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) - bytes.begin() + static_cast<std::ptrdiff_t>(pixel.sampleLocations.size() + 1u + sizeof(pixel.sampleOrder)); EraseDefaultTargetMerge(bytes, fusedMerge); bytes.resize(bytes.size() - 4u); @@ -819,6 +831,7 @@ int main() { return false; }; auto version46Dither = twoSample; + version46Dither.pop_back(); version46Dither[4] = 46u; Require(rejectsDither(version46Dither)); version46Dither[ditherByte] = 1u; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 74294bab9d..b886f83b1e 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -113,6 +113,7 @@ - [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`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. 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 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. 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. `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. - 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. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. A rect-list draw takes them from its own generated tessellation stages; its test passes on MoltenVK 1.4.2 (Apple M2), RADV (Radeon RX 7900 XT, Mesa 25.2.8), lavapipe, and on Windows on an RTX 3080 (NVIDIA 617.42) and Radeon RDNA 2 iGPUs (AMD 32.0.21045 and 32.0.21030); no title was run through it except Ruffle PPSA68091 on the M2. Tessellation and mesh pipelines, primitives other than triangles, triangle fans and primitive restart without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- On a device that offers `VK_KHR_fragment_shader_barycentric` built-ins but not its per-vertex inputs (MoltenVK: its MSL step rejects `PerVertexKHR` and takes one barycentric built-in per shader), the [host interpolation check](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) also accepts I/J reads left after the `v_interp_p1_f32`/`v_interp_p2_f32` pairs are lowered, as long as they are all perspective or all linear and read at the fragment's default position, and [reads them from the built-in](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp). Raw vertex parameters and I/J at an offset or a sample position still take the generated stages. Apple GPUs return perspective weights as `b/w`, not divided by their sum, unless the fragment function reads `[[position]]`, so each perspective read keeps `min(b, b + |gl_FragCoord.w|)`; `agc_driver_pixel_barycentric_emulation` checks the weights against the reference on such a device. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` skips if it does. - [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs), and with neg and abs modifiers over 16 values (13 of those, -1.5, 2.75 and 1 - 2^-53). Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_add_f64 and v_mul_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field; the PS5's gfx1013 is not measured. Inputs flush to a zero of their sign when bit 0 is clear (0 and 2). When bit 1 is clear (0 and 1), a sum below 2^-1022, or a product whose exact value rounded to 53 significant bits with an unbounded exponent is below 2^-1022, flushes to a zero of its sign, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126) rounds to 2^-1022 and is kept. Measured on RDNA2 (gfx1031, RX 6700 XT, AMD Windows driver 32.0.21045.5002, HIP) at IEEE=0 and 1, denorm16 0..3, over 576 pairs of 24 f64 values (zeros, denormals, minimum normals, values next to 1 and to 2^-1022, products just below 2^-1022, the largest finite value, infinities), with neg, abs and clamp. `v_fma_f64`, `v_div_fmas_f64` and shaders without a known float mode keep the previous lowering. From 661be6f367b06750b7a31cb3e99f6a0e399b23bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Sat, 24 Oct 2026 01:01:12 +0200 Subject: [PATCH 311/702] fix(shader): strip the barycentric flag in the version 43 request fixtures Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 3a3660696c..3d4fd33ab0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -6018,11 +6018,11 @@ void verifyVertexInfoVersion19() { const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 2u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes)), "the color target merge is not the 40 bytes the version 46 pixel info adds after the sample order"); require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); - // A version 43 request: no color target merge, and no fused arithmetic flag, pixel function LDS flag or storage size at the end. + // A version 43 request: no color target merge, and no fused arithmetic flag, pixel function LDS flag, storage size or barycentric built-ins flag at the end. const auto version43Bytes = [&](std::vector<std::uint8_t> bytes) { require(bytes.end() - bytes.begin() > mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes) && std::equal(plainPixel.begin() + mergeOffset, plainPixel.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes), bytes.begin() + mergeOffset), "a replay fixture does not hold the default color target merge after the sample order"); bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes)); - bytes.resize(bytes.size() - sizeof(std::uint32_t) - 2u); + bytes.resize(bytes.size() - sizeof(std::uint32_t) - 3u); return bytes; }; auto version42 = version43Bytes(plainPixel); @@ -6043,13 +6043,12 @@ void verifyVertexInfoVersion19() { orderedVersion34.pop_back(); orderedVersion34.pop_back(); orderedVersion34.pop_back(); - orderedVersion34.pop_back(); orderedVersion34.erase(orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset - 1u), orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)); orderedVersion34[4] = 34u; const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); auto orderedVersion30 = version43Bytes(orderedBytes); - orderedVersion30.resize(orderedVersion30.size() - 1u - sizeof(std::uint32_t)); + orderedVersion30.resize(orderedVersion30.size() - 4u); orderedVersion30.erase(orderedVersion30.begin() + static_cast<std::ptrdiff_t>(blendOffset + 4u), orderedVersion30.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)); orderedVersion30[4] = 30u; const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); @@ -6058,7 +6057,6 @@ void verifyVertexInfoVersion19() { orderedSpanned.target.spannedWaves = true; auto orderedVersion32 = version43Bytes(decodeBase64(serializer.Serialize(orderedSpanned))); orderedVersion32.pop_back(); - orderedVersion32.pop_back(); require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); orderedVersion32.pop_back(); orderedVersion32.pop_back(); @@ -6083,7 +6081,6 @@ void verifyVertexInfoVersion19() { version30.pop_back(); version30.pop_back(); version30.pop_back(); - version30.pop_back(); version30.erase(version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version30[4] = 30u; const auto replay30 = serializer.Deserialize(encodeBase64(version30)); @@ -6092,7 +6089,6 @@ void verifyVertexInfoVersion19() { spanned.target.spannedWaves = true; auto version32 = version43Bytes(decodeBase64(serializer.Serialize(spanned))); version32.pop_back(); - version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -6105,11 +6101,9 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.pop_back(); version29.pop_back(); - version29.pop_back(); version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); auto version36 = version43Bytes(sampledBytes); version36.pop_back(); - version36.pop_back(); version36.erase(version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -6122,7 +6116,6 @@ void verifyVertexInfoVersion19() { version28.pop_back(); version28.pop_back(); version28.pop_back(); - version28.pop_back(); version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); @@ -6132,7 +6125,6 @@ void verifyVertexInfoVersion19() { version27.pop_back(); version27.pop_back(); version27.pop_back(); - version27.pop_back(); version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); @@ -6142,7 +6134,6 @@ void verifyVertexInfoVersion19() { legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); - legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); From 03f7c1134f7ae006100d37cbe6e20976a6f0376f Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:03:48 +0200 Subject: [PATCH 312/702] fix(shader): keep the position read of barycentric built-ins on MoltenVK The I/J reads take the BaryCoordSmooth input, but gl_FragCoord was declared only for the offset kinds, so its w was read as the constant 0 and the fragment function never read [[position]]; Apple GPUs then return the weights unnormalized. Declare it with the built-in, and select on gl_FragCoord.w > 0, which a compiler cannot fold as it can min(b, b + |w|). --- .../recompiler/Optimization/src/ShaderInfoCollector.cpp | 2 +- .../recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp | 8 +++----- docs/dev/debt/shader.md | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index d93f1bbf58..272d201362 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -425,13 +425,13 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { break; case StageInputKind::BaryCoordSmooth: AddInput(info, StageInputKind::BaryCoordSmooth, 0, 3, "gl_BaryCoordKHR"); + if (program.Metadata().barycentricBuiltinsUnnormalized) AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); break; case StageInputKind::BaryCoordSmoothCenter: case StageInputKind::BaryCoordSmoothSample: info.smoothOffsetBarycentrics = true; AddInput(info, StageInputKind::BaryCoordSmooth, 0, 3, "gl_BaryCoordKHR"); if (kind == StageInputKind::BaryCoordSmoothSample && program.Metadata().pixelIteratesSamples) AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID"); - if (program.Metadata().barycentricBuiltinsUnnormalized) AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); break; case StageInputKind::BaryCoordNoPerspective: case StageInputKind::BaryCoordNoPerspectiveCenter: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 97ea701cc0..7dd30f7185 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -283,12 +283,10 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::BaryCoordSmooth && state.program.Metadata().barycentricBuiltinsUnnormalized) { const auto fragCoordW = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeF32(state), fragCoordW, EmitBuiltinU32(state, StageInputKind::FragCoord, 3u)); - const auto magnitude = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), magnitude, GlslStd450(state), GLSLstd450FAbs, fragCoordW); - const auto bound = state.module.AllocateId(); - state.module.AddFunction(spv::OpFAdd, TypeF32(state), bound, value, magnitude); + const auto rasterized = state.module.AllocateId(); + state.module.AddFunction(spv::OpFOrdGreaterThan, TypeBool(state), rasterized, fragCoordW, ConstantF32Value(state, 0.0f)); const auto kept = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), kept, GlslStd450(state), GLSLstd450FMin, value, bound); + state.module.AddFunction(spv::OpSelect, TypeF32(state), kept, rasterized, value, ConstantF32Value(state, 0.0f)); state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, kept); return bits; } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index b886f83b1e..ccbb35e7be 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -113,7 +113,7 @@ - [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`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. 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 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. 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. `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. - 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. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. A rect-list draw takes them from its own generated tessellation stages; its test passes on MoltenVK 1.4.2 (Apple M2), RADV (Radeon RX 7900 XT, Mesa 25.2.8), lavapipe, and on Windows on an RTX 3080 (NVIDIA 617.42) and Radeon RDNA 2 iGPUs (AMD 32.0.21045 and 32.0.21030); no title was run through it except Ruffle PPSA68091 on the M2. Tessellation and mesh pipelines, primitives other than triangles, triangle fans and primitive restart without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. -- On a device that offers `VK_KHR_fragment_shader_barycentric` built-ins but not its per-vertex inputs (MoltenVK: its MSL step rejects `PerVertexKHR` and takes one barycentric built-in per shader), the [host interpolation check](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) also accepts I/J reads left after the `v_interp_p1_f32`/`v_interp_p2_f32` pairs are lowered, as long as they are all perspective or all linear and read at the fragment's default position, and [reads them from the built-in](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp). Raw vertex parameters and I/J at an offset or a sample position still take the generated stages. Apple GPUs return perspective weights as `b/w`, not divided by their sum, unless the fragment function reads `[[position]]`, so each perspective read keeps `min(b, b + |gl_FragCoord.w|)`; `agc_driver_pixel_barycentric_emulation` checks the weights against the reference on such a device. +- On a device that offers `VK_KHR_fragment_shader_barycentric` built-ins but not its per-vertex inputs (MoltenVK's MSL step rejects `PerVertexKHR`), the [host interpolation check](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) also accepts I/J reads left after the `v_interp_p1_f32`/`v_interp_p2_f32` pairs are lowered, as long as they are all perspective or all linear and read at the fragment's default position, and [reads them from the built-in](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp). Raw vertex parameters and I/J at an offset or a sample position still take the generated stages. Apple GPUs return perspective weights as `b/w`, not divided by their sum, unless the compiled function reads `[[position]]`, so each perspective read returns `gl_FragCoord.w > 0 ? b : 0`, which the compiler cannot fold (`w` is positive for every rasterized fragment); `agc_driver_pixel_barycentric_emulation` checks this. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` skips if it does. - [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs), and with neg and abs modifiers over 16 values (13 of those, -1.5, 2.75 and 1 - 2^-53). Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_add_f64 and v_mul_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field; the PS5's gfx1013 is not measured. Inputs flush to a zero of their sign when bit 0 is clear (0 and 2). When bit 1 is clear (0 and 1), a sum below 2^-1022, or a product whose exact value rounded to 53 significant bits with an unbounded exponent is below 2^-1022, flushes to a zero of its sign, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126) rounds to 2^-1022 and is kept. Measured on RDNA2 (gfx1031, RX 6700 XT, AMD Windows driver 32.0.21045.5002, HIP) at IEEE=0 and 1, denorm16 0..3, over 576 pairs of 24 f64 values (zeros, denormals, minimum normals, values next to 1 and to 2^-1022, products just below 2^-1022, the largest finite value, infinities), with neg, abs and clamp. `v_fma_f64`, `v_div_fmas_f64` and shaders without a known float mode keep the previous lowering. From 63c5b74806b22e76619a7299d96845c8e316cb1d Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 06:23:31 +0000 Subject: [PATCH 313/702] docs(libSceNpGameIntent): note the info size and intent data offset six titles use Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceNpGameIntent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpGameIntent.md b/docs/dev/debt/libSceNpGameIntent.md index a912f37f7e..6291b2f110 100644 --- a/docs/dev/debt/libSceNpGameIntent.md +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -11,4 +11,4 @@ - [sceNpAppLauncherLaunchApp](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent, NID `GbayQ7DO8jA`) - its recovered signature is `(int32_t, const char*, const char*)`: an unhandled first argument, a required non-empty string and an optional string rejected past 0x2000 bytes, returning `0x80553A02` before `sceNpAppLauncherInitialize` and `0x80553A04` on invalid arguments. The launch itself is not implemented and throws; the firmware tail calls an internal launch through an extra library context and remaps its `0x805538xx` errors into `0x80553Axx` - [sceNpGameIntentGetUdsNpComId, sceNpGameIntentLaunchApp, sceNpGameIntentLaunchApp2, sceNpGameIntentNotifyAppLaunched, sceNpSessionServiceGetJoinedPartySession, sceNpSessionServiceGetRunningGamePlayerSession and sceNpSessionServiceSendPlayerSessionInvitations](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpGameIntent.c). They are declared without parameters solely to resolve known imports and make no ABI claim. - [sceNpGameIntentInitParamInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L17) `SceNpGameIntentInitParam`; setting `size`, zeroing reserved bytes, and throwing on null pointer follow Sony SDK `*ParamInit` conventions but are unverified on retail hardware -- [sceNpGameIntentInfoInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L28) `SceNpGameIntentInfo`; setting `size = 0x560`, `user_id = -1`, zeroing fields, and throwing on null pointer follow fpPS4 defaults, but reported firmware 13.42 behavior requires `0x4140` for `sceNpGameIntentReceiveIntent`, so the helper's `0x560` size does not match retail receiver defaults; the complete retail structure layout and retail behavior of `sceNpGameIntentInfoInit` remain unverified +- [sceNpGameIntentInfoInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L28) `SceNpGameIntentInfo`; setting `size = 0x560`, `user_id = -1`, zeroing fields, and throwing on null pointer follow fpPS4 defaults, but reported firmware 13.42 behavior requires `0x4140` for `sceNpGameIntentReceiveIntent`, so the helper's `0x560` size does not match retail receiver defaults; the complete retail structure layout and retail behavior of `sceNpGameIntentInfoInit` remain unverified. Title code disagrees with that layout: before their one `sceNpGameIntentReceiveIntent` call, PPSA01494, PPSA03984, PPSA21564, PPSA30839, PPSA31162 and PPSA34502 zero the 0x4138 bytes after an 8-byte size (five store size 0x4140); after it they read the type at 0xc but pass 0x134, not 0x153, as the intent data. None of them calls `sceNpGameIntentInfoInit` From 603a540fa7dcdfa3cfd7991e34ac583ae30064de Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 06:29:16 +0000 Subject: [PATCH 314/702] docs(libSceAudioOut): scope the port state reads to the inspected instructions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAudioOut.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 8b4ab453e7..9df7d3913d 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -29,7 +29,7 @@ - [Legacy vibration ports](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp): PPSA99902's [public input wrapper](https://github.com/Nazky/UnleashedRecomp-Prospero/blob/2ce067a1d51bb58670c7ee4a3aed893c504da805/UnleashedRecomp/hid/driver/sdl_hid.cpp#L316) opens type 10 with 256 stereo S16 or F32 frames at 48000 Hz. Mono duplicates its channel as AudioOut2 does; wider vibration layouts throw. Native controller output uses AudioOut2's four-channel layout, or its six-channel PulseAudio mapping. Without a controller sound card the existing RMS motor transform is applied to segments of at most 256 frames, scheduled by the legacy queue's frame count and rate; drain and close remove only that port's segments. Concurrent sources combine by the maximum per motor, and the pad consumer uses this approximation only for controllers other than DualSense. This host timing and motor model is not a console measurement or waveform reproduction; legacy native output is unmeasured on hardware, and user routing retains the single-controller limitation. AudioOut2 retains its separate 80 ms peak hold. - [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the native wrapper consumes a 64-bit device ID, port type, sample count, frequency and format from argument slots 1, 2, 4, 5 and 6. Slot 3 is discarded, with the common open API index position preserved. Device enumeration and mapping to host outputs remain unimplemented; the throwing body is unchanged. -- [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator. A payload process on a PS5 (firmware 13.60), which is not the foreground application, read output 0, volume -1 and reroute counter 5 for its main ports, and 6 channels for an 8-channel port on a 5.1 system, so a title's values are unverified, including the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change. Titles read the output (PPSA01494, PPSA21564), channels and volume (PPSA21564, volume against 0) and flags bit 0 (PPSA03984, PPSA21564, PPSA30839, PPSA31162); none reads the reroute counter. +- [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator. A payload process on a PS5 (firmware 13.60), which is not the foreground application, read output 0, volume -1 and reroute counter 5 for its main ports, and 6 channels for an 8-channel port on a 5.1 system, so a title's values are unverified, including the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change. In the 30 instructions after each call, titles read the output (PPSA01494, PPSA21564), channels and volume (PPSA21564) and flags bit 0 (PPSA03984, PPSA21564, PPSA30839, PPSA31162), not the reroute counter. - [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - The speaker info reports type 0 with available bits `0x3`, the front left speaker at +30 degrees and the front right one at -30, and -360 for the azimuth and elevation of every other slot. On that console the type followed the HDMI device type and the available bits its channels: type 0 with the device set to TV, 1 to AV amplifier and 2 to sound bar, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) names television, AV receiver and sound bar (3 is headphones there), and available bits `0x3` for 2 channels and `0x3F` for 5.1 (centre at 0, surround pair at +110 and -110, -360 for the LFE). - [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - The front pair and the empty slots read the same in every setting; 7.1 on the AV amplifier reported 2 channels on that console's HDMI chain. The host output is always reported as a stereo television here. The console also refuses flag 1 below a per-process libkernel value whose source is unidentified; BunnyFl measured the boundary between `0x14FFFFF` and `0x1500000`, and it is not modelled here. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on these values (#4317), but which field matters has not been isolated - [sceAudioOut2InitializeEx and sceAudioOut2LoInit repeats](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - a repeated `sceAudioOut2InitializeEx` or `sceAudioOut2LoInit` returns 0, and either one after `sceAudioOut2Initialize` or `sceAudioOutInit` returns 0 too; the console was not measured for these two. On PS5 firmware 13.60 a repeated `sceAudioOut2Initialize` returns `0x80268004` and a repeated `sceAudioOutInit` `0x8026000e`, and `sceAudioOutInit` after `sceAudioOut2Initialize` still returns 0, which AnyPS5 follows. From 4912e4c7a913216365f376b07cb694eea5470760 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 06:33:48 +0000 Subject: [PATCH 315/702] test(libkernel): cover the APR ForEach resolves with and without a prefix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestAmpr.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 7b5bc20439..42fe6202c6 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -651,6 +651,29 @@ void TestResolveFailures() { std::remove(paths[0]); } +void TestResolveForEachVariants() { + const char* paths[]{"ampr_foreach_existing.bin", "ampr_foreach_missing.bin"}; + const char* names[]{"foreach_existing.bin", "foreach_missing.bin"}; + { std::ofstream file(paths[0], std::ios::binary); file << "data"; } + std::uint32_t expected = 0; + std::uint32_t failed = 99; + Require(sceKernelAprResolveFilepathsToIds(paths, 1, &expected, &failed) == 0); + std::uint32_t ids[]{123, 123}; + int results[]{123, 123}; + Require(sceKernelAprResolveFilepathsToIdsForEach(paths, 2, ids, results) == 1); + Require(ids[0] == expected && ids[1] == 0xffffffffu && results[0] == 0 && results[1] == static_cast<int>(0x80020002)); + ids[0] = ids[1] = 123; + results[0] = results[1] = 123; + Require(sceKernelAprResolveFilepathsWithPrefixToIdsForEach("ampr_", names, 2, ids, results) == 1); + Require(ids[0] == expected && ids[1] == 0xffffffffu && results[0] == 0 && results[1] == static_cast<int>(0x80020002)); + std::uint64_t sizes[]{123, 123}; + ids[0] = ids[1] = 123; + results[0] = results[1] = 123; + Require(sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizesForEach("ampr_", names, 2, ids, sizes, results) == 1); + Require(ids[0] == expected && ids[1] == 0xffffffffu && sizes[0] == 4 && sizes[1] == 0 && results[0] == 0 && results[1] == static_cast<int>(0x80020002)); + std::remove(paths[0]); +} + void TestMalformedCommandSizes() { const auto requireFailure = [](Apr::Opcode opcode, std::uint32_t recordBytes, std::uint32_t bufferBytes, const char* expected) { std::array<std::uint8_t, 16> memory{}; @@ -1385,6 +1408,7 @@ int main() { TestReadPastEndOfFileZeroFills(); TestReadAtUnseekableOffsetThrows(); TestResolveFailures(); + TestResolveForEachVariants(); TestMalformedCommandSizes(); TestResolveSymbolicLink(); TestGatherScatter(); From fd42fb30cc2545d0f892ebf72fbd0de2b8816ef2 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:03:56 +0000 Subject: [PATCH 316/702] feat(tools): summarize the failing tests of a sanitizer run Add tools/sanitizer_report.py, which reads the JUnit file that ctest --output-junit writes, counts passed, skipped and failed tests, lists the failed ones in a Markdown summary and exits with 1 when any test failed. A test that ctest did not run counts as failed, and a disabled test as skipped. --- tools/sanitizer_report.py | 61 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tools/sanitizer_report.py diff --git a/tools/sanitizer_report.py b/tools/sanitizer_report.py new file mode 100644 index 0000000000..0a7de62f54 --- /dev/null +++ b/tools/sanitizer_report.py @@ -0,0 +1,61 @@ +import argparse +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +FAILED_STATUSES = ("fail", "notrun") +SKIPPED_STATUSES = ("disabled",) + + +def results(path): + root = ET.parse(path).getroot() + if root.tag not in ("testsuite", "testsuites"): + raise ValueError(f"{path}: expected a JUnit testsuite or testsuites root, got {root.tag!r}") + found = {} + for case in root.iter("testcase"): + name = case.get("name") + if not name: + raise ValueError(f"{path}: testcase has no name") + if case.find("skipped") is not None or case.get("status") in SKIPPED_STATUSES: + outcome = "skipped" + elif case.get("status") in FAILED_STATUSES or case.find("failure") is not None or case.find("error") is not None: + outcome = "failed" + else: + outcome = "passed" + found[name] = outcome + return found + + +def summarize(title, found): + counts = {outcome: sum(1 for value in found.values() if value == outcome) for outcome in ("passed", "skipped", "failed")} + failed = sorted(name for name, outcome in found.items() if outcome == "failed") + lines = [f"### {title}", "", + f"{len(found)} tests: {counts['passed']} passed, {counts['skipped']} skipped, {counts['failed']} failed.", ""] + if failed: + lines += ["| Failed test |", "| - |"] + [f"| `{name}` |" for name in failed] + [""] + return "\n".join(lines) + "\n", bool(failed) + + +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace") + parser = argparse.ArgumentParser(description="summarize a sanitizer test run and fail when a test failed") + parser.add_argument("junit", type=Path, help="the file written by ctest --output-junit") + parser.add_argument("--title", default="Sanitizer run", help="heading of the summary") + parser.add_argument("--summary", type=Path, help="also append the summary to this file, such as $GITHUB_STEP_SUMMARY") + args = parser.parse_args(argv) + try: + text, failing = summarize(args.title, results(args.junit)) + except (OSError, ET.ParseError, ValueError) as error: + print(f"sanitizer_report: {error}", file=sys.stderr) + return 2 + sys.stdout.write(text) + if args.summary: + with args.summary.open("a", encoding="utf-8") as summary: + summary.write(text) + return 1 if failing else 0 + + +if __name__ == "__main__": + sys.exit(main()) From a2ab196682cd7010f66cfd6c607d4278064e43b9 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:04:20 +0000 Subject: [PATCH 317/702] test(tools): cover the sanitizer run summary Check how sanitizer_report classifies passed, failed, skipped, not run and disabled tests, the counts and sorted table of the summary, the exit code for runs with and without failures, the exit code 2 for unreadable or malformed reports, and that --summary appends to an existing file. --- tools/tests/test_sanitizer_report.py | 84 ++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tools/tests/test_sanitizer_report.py diff --git a/tools/tests/test_sanitizer_report.py b/tools/tests/test_sanitizer_report.py new file mode 100644 index 0000000000..97664358df --- /dev/null +++ b/tools/tests/test_sanitizer_report.py @@ -0,0 +1,84 @@ +from pathlib import Path +import contextlib +import io +import sys +import tempfile +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import sanitizer_report + + +def case(name, status="run", body=""): + return f'<testcase name="{name}" classname="{name}" time="0.1" status="{status}">{body}</testcase>' + + +def suite(*cases): + return f'<testsuite name="(empty)" tests="{len(cases)}">' + "".join(cases) + "</testsuite>" + + +class SanitizerReportTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + + def write(self, text, name="ctest.xml"): + path = Path(self.directory.name) / name + path.write_text(text, encoding="utf-8") + return path + + def run_main(self, *args): + stdout, stderr = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + code = sanitizer_report.main([str(arg) for arg in args]) + return code, stdout.getvalue(), stderr.getvalue() + + def test_classifies_passed_skipped_failed_and_not_run_tests(self): + path = self.write(suite( + case("passes"), + case("fails", "fail", '<failure message=""/>'), + case("skips", "run", '<skipped message="SKIP_RETURN_CODE=77"/>'), + case("never_built", "notrun"), + case("switched_off", "disabled"), + )) + self.assertEqual(sanitizer_report.results(path), { + "passes": "passed", "fails": "failed", "skips": "skipped", "never_built": "failed", "switched_off": "skipped", + }) + + def test_summary_counts_and_sorts_the_failed_tests(self): + text, failing = sanitizer_report.summarize("ASan", {"b": "failed", "a": "failed", "c": "passed", "d": "skipped"}) + self.assertTrue(failing) + self.assertEqual(text, "### ASan\n\n4 tests: 1 passed, 1 skipped, 2 failed.\n\n" + "| Failed test |\n| - |\n| `a` |\n| `b` |\n\n") + + def test_summary_without_failures_has_no_table(self): + text, failing = sanitizer_report.summarize("ASan", {"a": "passed"}) + self.assertFalse(failing) + self.assertNotIn("| Failed test |", text) + + def test_exit_code_follows_the_failed_tests(self): + self.assertEqual(self.run_main(self.write(suite(case("passes"))))[0], 0) + code, stdout, _ = self.run_main(self.write(suite(case("fails", "fail", '<failure message=""/>')))) + self.assertEqual(code, 1) + self.assertIn("| `fails` |", stdout) + + def test_unreadable_reports_exit_with_2(self): + for text in ("<results/>", suite('<testcase status="run"/>'), "not xml"): + with self.subTest(text=text): + code, stdout, stderr = self.run_main(self.write(text)) + self.assertEqual(code, 2) + self.assertEqual(stdout, "") + self.assertTrue(stderr.startswith("sanitizer_report: ")) + code, _, stderr = self.run_main(Path(self.directory.name) / "missing.xml") + self.assertEqual(code, 2) + + def test_summary_file_is_appended_to(self): + summary = self.write("existing\n", "summary.md") + code, stdout, _ = self.run_main(self.write(suite(case("passes"))), "--title", "Nightly", "--summary", summary) + self.assertEqual(code, 0) + self.assertEqual(summary.read_text(encoding="utf-8"), "existing\n" + stdout) + self.assertTrue(stdout.startswith("### Nightly\n")) + + +if __name__ == "__main__": + unittest.main() From aab9334b9ebd6b417cc69a2088315f439667b438 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:04:48 +0000 Subject: [PATCH 318/702] build: register the sanitizer report test with ctest Run tools/tests/test_sanitizer_report.py as the sanitizer_report test next to check_skips, so check_test_registration --scripts finds a runner for it. --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index a9b92c348b..3f75497287 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -228,6 +228,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py) + add_test(NAME sanitizer_report COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_sanitizer_report.py) add_test(NAME select_tests COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py) add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE} From 4fa6238a7cb708c00391aa84b784be218290ab4c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:05:18 +0000 Subject: [PATCH 319/702] feat(tools): accept known sanitizer failures with their reasons sanitizer_report.py --known reads a text file with one known failure per line: the test name, the comma-separated sanitizers it fails under and the reason. With --sanitizer naming the sanitizers of the build, a failure of a listed test moves to a Known failure table with its reason and no longer fails the run. Blank lines are ignored; a line without a reason, an unknown sanitizer and a duplicate test and sanitizer pair are errors that name the line. --- tools/sanitizer_report.py | 46 +++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/tools/sanitizer_report.py b/tools/sanitizer_report.py index 0a7de62f54..625cc1bfe4 100644 --- a/tools/sanitizer_report.py +++ b/tools/sanitizer_report.py @@ -5,6 +5,7 @@ from pathlib import Path FAILED_STATUSES = ("fail", "notrun") SKIPPED_STATUSES = ("disabled",) +SANITIZERS = ("address", "undefined", "thread") def results(path): @@ -26,13 +27,43 @@ def results(path): return found -def summarize(title, found): +def load_known(path, sanitizers): + known, seen = {}, set() + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if not line.strip(): + continue + location = f"{path}:{number}" + fields = line.split(None, 2) + if len(fields) < 3: + raise ValueError(f"{location}: expected a test name, its sanitizers and a reason") + test, names, reason = fields[0], fields[1].split(","), fields[2].strip() + if any(name not in SANITIZERS for name in names): + raise ValueError(f"{location}: sanitizers must be a comma-separated list of {', '.join(SANITIZERS)}") + for name in names: + if (test, name) in seen: + raise ValueError(f"{location}: {test} is already listed for {name}") + seen.add((test, name)) + if set(names) & set(sanitizers): + known[test] = reason + return known + + +def cell(text): + return " ".join(text.split()).replace("|", "\\|") + + +def summarize(title, found, known=None): + known = known or {} counts = {outcome: sum(1 for value in found.values() if value == outcome) for outcome in ("passed", "skipped", "failed")} - failed = sorted(name for name, outcome in found.items() if outcome == "failed") + failed = sorted(name for name, outcome in found.items() if outcome == "failed" and name not in known) + expected = sorted(name for name, outcome in found.items() if outcome == "failed" and name in known) + tally = f"{counts['failed']} failed" + (f" ({len(expected)} known)" if expected else "") lines = [f"### {title}", "", - f"{len(found)} tests: {counts['passed']} passed, {counts['skipped']} skipped, {counts['failed']} failed.", ""] + f"{len(found)} tests: {counts['passed']} passed, {counts['skipped']} skipped, {tally}.", ""] if failed: lines += ["| Failed test |", "| - |"] + [f"| `{name}` |" for name in failed] + [""] + if expected: + lines += ["| Known failure | Reason |", "| - | - |"] + [f"| `{name}` | {cell(known[name])} |" for name in expected] + [""] return "\n".join(lines) + "\n", bool(failed) @@ -44,9 +75,16 @@ def main(argv=None): parser.add_argument("junit", type=Path, help="the file written by ctest --output-junit") parser.add_argument("--title", default="Sanitizer run", help="heading of the summary") parser.add_argument("--summary", type=Path, help="also append the summary to this file, such as $GITHUB_STEP_SUMMARY") + parser.add_argument("--known", type=Path, + help="known failures, one per line: test name, comma-separated sanitizers and the reason") + parser.add_argument("--sanitizer", action="append", choices=SANITIZERS, default=[], + help="sanitizer the build used; repeat for several. Selects the --known entries that apply") args = parser.parse_args(argv) + if args.known and not args.sanitizer: + parser.error("--known needs at least one --sanitizer") try: - text, failing = summarize(args.title, results(args.junit)) + known = load_known(args.known, args.sanitizer) if args.known else {} + text, failing = summarize(args.title, results(args.junit), known) except (OSError, ET.ParseError, ValueError) as error: print(f"sanitizer_report: {error}", file=sys.stderr) return 2 From b8eab9062024c1512d7e6ce96ae101498c1c9f79 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:05:46 +0000 Subject: [PATCH 320/702] test(tools): cover known sanitizer failures Check that known failures apply only to the sanitizers they name and that blank lines are skipped, that a listed failure moves to the Known failure table with its reason escaped for Markdown and stops failing the run while an unlisted one still fails it, that --known without --sanitizer is a usage error, and that malformed lines are rejected with exit code 2 and their line number. --- tools/tests/test_sanitizer_report.py | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tools/tests/test_sanitizer_report.py b/tools/tests/test_sanitizer_report.py index 97664358df..9a5e6cb52a 100644 --- a/tools/tests/test_sanitizer_report.py +++ b/tools/tests/test_sanitizer_report.py @@ -79,6 +79,62 @@ class SanitizerReportTests(unittest.TestCase): self.assertEqual(summary.read_text(encoding="utf-8"), "existing\n" + stdout) self.assertTrue(stdout.startswith("### Nightly\n")) + def write_known(self, *lines): + return self.write("\n".join(lines) + "\n", "known.txt") + + def test_known_failures_apply_only_to_the_selected_sanitizers(self): + known = self.write_known( + "fiber address stack switch", + "", + "racy thread interceptor gap", + "both address,undefined shared reason ", + ) + self.assertEqual(sanitizer_report.load_known(known, ["address", "undefined"]), + {"fiber": "stack switch", "both": "shared reason"}) + self.assertEqual(sanitizer_report.load_known(known, ["thread"]), {"racy": "interceptor gap"}) + + def test_known_failures_move_to_their_own_table_and_keep_the_run_passing(self): + found = {"fiber": "failed", "new_bug": "failed", "ok": "passed"} + text, failing = sanitizer_report.summarize("ASan", found, {"fiber": "guest stack | switch"}) + self.assertTrue(failing) + self.assertIn("3 tests: 1 passed, 0 skipped, 2 failed (1 known).", text) + self.assertIn("| Failed test |\n| - |\n| `new_bug` |\n", text) + self.assertIn("| Known failure | Reason |\n| - | - |\n| `fiber` | guest stack \\| switch |\n", text) + text, failing = sanitizer_report.summarize("ASan", {"fiber": "failed", "ok": "passed"}, {"fiber": "guest stack switch"}) + self.assertFalse(failing) + self.assertNotIn("| Failed test |", text) + + def test_main_passes_when_every_failure_is_known(self): + junit = self.write(suite(case("fiber", "fail", '<failure message=""/>'), case("ok"))) + known = self.write_known("fiber address stack switch") + self.assertEqual(self.run_main(junit, "--known", known, "--sanitizer", "address")[0], 0) + self.assertEqual(self.run_main(junit, "--known", known, "--sanitizer", "thread")[0], 1) + + def test_known_needs_a_sanitizer(self): + known = self.write_known("") + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as raised: + sanitizer_report.main([str(self.write(suite(case("ok")))), "--known", str(known)]) + self.assertEqual(raised.exception.code, 2) + + def test_malformed_known_failures_are_rejected_with_their_line(self): + broken = [ + ("fiber address",), + ("fiber",), + ("fiber memory stack switch",), + ("fiber address, stack switch",), + ("fiber ,address stack switch",), + ("fiber address stack switch", "fiber address again"), + ("ok address fine", "fiber address,address twice"), + ] + junit = self.write(suite(case("ok"))) + for lines in broken: + with self.subTest(lines=lines): + code, _, stderr = self.run_main(junit, "--known", self.write_known(*lines), "--sanitizer", "address") + self.assertEqual(code, 2) + self.assertIn(f"known.txt:{len(lines)}: ", stderr) + code, _, _ = self.run_main(junit, "--known", Path(self.directory.name) / "missing.txt", "--sanitizer", "address") + self.assertEqual(code, 2) + if __name__ == "__main__": unittest.main() From 9685e5786e294875f722571c38abaa32427c50e8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:05:58 +0000 Subject: [PATCH 321/702] feat(tools): report known sanitizer failures that pass or did not run List the known failures of the selected sanitizers whose test passed, so a fixed test can leave the list, and those that are not in the run at all, such as a renamed test. Neither affects the exit code. --- tools/sanitizer_report.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/sanitizer_report.py b/tools/sanitizer_report.py index 625cc1bfe4..08eb1d3a2e 100644 --- a/tools/sanitizer_report.py +++ b/tools/sanitizer_report.py @@ -64,6 +64,12 @@ def summarize(title, found, known=None): lines += ["| Failed test |", "| - |"] + [f"| `{name}` |" for name in failed] + [""] if expected: lines += ["| Known failure | Reason |", "| - | - |"] + [f"| `{name}` | {cell(known[name])} |" for name in expected] + [""] + recovered = sorted(name for name in known if found.get(name) == "passed") + if recovered: + lines += ["| Listed as known but passed | Reason |", "| - | - |"] + [f"| `{name}` | {cell(known[name])} |" for name in recovered] + [""] + absent = sorted(name for name in known if name not in found) + if absent: + lines += ["| Listed as known but not in this run |", "| - |"] + [f"| `{name}` |" for name in absent] + [""] return "\n".join(lines) + "\n", bool(failed) From 851d4498e13b7e82dad0a07dbceb68b992cbd994 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:06:11 +0000 Subject: [PATCH 322/702] test(tools): cover known sanitizer failures that pass or did not run Check that a listed test that passed appears in the passed table with its reason, that a listed test missing from the run appears in the not-run table, that a skipped listed test appears in neither, and that a run whose known failures all still fail has neither table. --- tools/tests/test_sanitizer_report.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/tests/test_sanitizer_report.py b/tools/tests/test_sanitizer_report.py index 9a5e6cb52a..c89ee24be6 100644 --- a/tools/tests/test_sanitizer_report.py +++ b/tools/tests/test_sanitizer_report.py @@ -135,6 +135,19 @@ class SanitizerReportTests(unittest.TestCase): code, _, _ = self.run_main(junit, "--known", Path(self.directory.name) / "missing.txt", "--sanitizer", "address") self.assertEqual(code, 2) + def test_known_failures_that_pass_or_did_not_run_are_listed_without_failing(self): + found = {"fixed": "passed", "still_broken": "failed", "skipped_now": "skipped"} + known = {"fixed": "was broken", "still_broken": "stack switch", "skipped_now": "device", "renamed": "old name"} + text, failing = sanitizer_report.summarize("ASan", found, known) + self.assertFalse(failing) + self.assertIn("| Listed as known but passed | Reason |\n| - | - |\n| `fixed` | was broken |\n", text) + self.assertIn("| Listed as known but not in this run |\n| - |\n| `renamed` |\n", text) + self.assertNotIn("`skipped_now`", text) + + def test_no_stale_tables_when_every_known_failure_still_fails(self): + text, _ = sanitizer_report.summarize("ASan", {"fiber": "failed"}, {"fiber": "stack switch"}) + self.assertNotIn("Listed as known but", text) + if __name__ == "__main__": unittest.main() From 59df5f3bb6310f710675506bf73a7c0b0515d0ba Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:06:45 +0000 Subject: [PATCH 323/702] feat(tools): quote the first sanitizer report of each failed test Read the system-out and system-err of each testcase and keep the first sanitizer line: the SUMMARY line of ASan, LSan or TSan, else their ERROR or WARNING line, else a UBSan runtime error line with its source location. When any unexpected failure has one, the failed test table gains a First sanitizer report column, shortened to 200 characters and escaped for Markdown. --- tools/sanitizer_report.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/tools/sanitizer_report.py b/tools/sanitizer_report.py index 08eb1d3a2e..593af2eba3 100644 --- a/tools/sanitizer_report.py +++ b/tools/sanitizer_report.py @@ -1,4 +1,5 @@ import argparse +import re import sys import xml.etree.ElementTree as ET from pathlib import Path @@ -6,6 +7,12 @@ from pathlib import Path FAILED_STATUSES = ("fail", "notrun") SKIPPED_STATUSES = ("disabled",) SANITIZERS = ("address", "undefined", "thread") +REPORT_LINES = ( + re.compile(r"SUMMARY: \w+Sanitizer: .*"), + re.compile(r"(?:ERROR|WARNING): \w+Sanitizer: .*"), + re.compile(r"(?:\S+ )?runtime error: .*"), +) +REPORT_LENGTH = 200 def results(path): @@ -27,6 +34,23 @@ def results(path): return found +def first_report(output): + for pattern in REPORT_LINES: + if match := pattern.search(output): + line = " ".join(match.group(0).split()) + return line if len(line) <= REPORT_LENGTH else line[:REPORT_LENGTH - 3] + "..." + return "" + + +def reports(path): + found = {} + for case in ET.parse(path).getroot().iter("testcase"): + output = "\n".join(case.findtext(channel) or "" for channel in ("system-out", "system-err")) + if report := first_report(output): + found[case.get("name")] = report + return found + + def load_known(path, sanitizers): known, seen = {}, set() for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): @@ -52,15 +76,18 @@ def cell(text): return " ".join(text.split()).replace("|", "\\|") -def summarize(title, found, known=None): - known = known or {} +def summarize(title, found, known=None, details=None): + known, details = known or {}, details or {} counts = {outcome: sum(1 for value in found.values() if value == outcome) for outcome in ("passed", "skipped", "failed")} failed = sorted(name for name, outcome in found.items() if outcome == "failed" and name not in known) expected = sorted(name for name, outcome in found.items() if outcome == "failed" and name in known) tally = f"{counts['failed']} failed" + (f" ({len(expected)} known)" if expected else "") lines = [f"### {title}", "", f"{len(found)} tests: {counts['passed']} passed, {counts['skipped']} skipped, {tally}.", ""] - if failed: + if failed and any(name in details for name in failed): + lines += ["| Failed test | First sanitizer report |", "| - | - |"] + lines += [f"| `{name}` | {cell(details.get(name, ''))} |" for name in failed] + [""] + elif failed: lines += ["| Failed test |", "| - |"] + [f"| `{name}` |" for name in failed] + [""] if expected: lines += ["| Known failure | Reason |", "| - | - |"] + [f"| `{name}` | {cell(known[name])} |" for name in expected] + [""] @@ -90,7 +117,7 @@ def main(argv=None): parser.error("--known needs at least one --sanitizer") try: known = load_known(args.known, args.sanitizer) if args.known else {} - text, failing = summarize(args.title, results(args.junit), known) + text, failing = summarize(args.title, results(args.junit), known, reports(args.junit)) except (OSError, ET.ParseError, ValueError) as error: print(f"sanitizer_report: {error}", file=sys.stderr) return 2 From 1c9757015bf4907ef5ce37f8aa35688ee52bac7a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:07:05 +0000 Subject: [PATCH 324/702] test(tools): cover the first sanitizer report of failed tests Check that the SUMMARY line wins over the ERROR line, that LSan and TSan lines and a UBSan runtime error with its source location are found, that output without a sanitizer line gives nothing, that long lines are shortened, and that the failed test table gains the report column only when a failure has a report. --- tools/tests/test_sanitizer_report.py | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tools/tests/test_sanitizer_report.py b/tools/tests/test_sanitizer_report.py index c89ee24be6..f92a4dc7e7 100644 --- a/tools/tests/test_sanitizer_report.py +++ b/tools/tests/test_sanitizer_report.py @@ -148,6 +148,47 @@ class SanitizerReportTests(unittest.TestCase): text, _ = sanitizer_report.summarize("ASan", {"fiber": "failed"}, {"fiber": "stack switch"}) self.assertNotIn("Listed as known but", text) + def test_first_report_prefers_the_summary_then_the_error_then_a_runtime_error(self): + asan = ("==7==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60d0\n" + "READ of size 16\n" + "SUMMARY: AddressSanitizer: heap-buffer-overflow (/build/tests/x+0x12) in Inspect\n") + self.assertEqual(sanitizer_report.first_report(asan), + "SUMMARY: AddressSanitizer: heap-buffer-overflow (/build/tests/x+0x12) in Inspect") + self.assertEqual(sanitizer_report.first_report("==7==ERROR: LeakSanitizer: detected memory leaks\n"), + "ERROR: LeakSanitizer: detected memory leaks") + self.assertEqual(sanitizer_report.first_report("WARNING: ThreadSanitizer: data race (pid=9)\n"), + "WARNING: ThreadSanitizer: data race (pid=9)") + self.assertEqual(sanitizer_report.first_report("start\n/src/Copy.cpp:35:16: runtime error: null pointer passed as argument 2\n"), + "/src/Copy.cpp:35:16: runtime error: null pointer passed as argument 2") + self.assertEqual(sanitizer_report.first_report("FAIL: plain assertion\n"), "") + + def test_first_report_is_shortened(self): + report = sanitizer_report.first_report("SUMMARY: AddressSanitizer: " + "x" * 400) + self.assertEqual(len(report), sanitizer_report.REPORT_LENGTH) + self.assertTrue(report.endswith("...")) + + def test_failed_tests_show_their_first_report(self): + junit = self.write(suite( + case("overread", "fail", '<failure message=""/><system-out>SUMMARY: AddressSanitizer: heap-buffer-overflow in A|B</system-out>'), + case("plain", "fail", '<failure message=""/><system-out>FAIL: assertion</system-out>'), + case("ub", "fail", '<failure message=""/><system-err>/a.cpp:1:2: runtime error: shift exponent 64</system-err>'), + )) + self.assertEqual(sanitizer_report.reports(junit), { + "overread": "SUMMARY: AddressSanitizer: heap-buffer-overflow in A|B", + "ub": "/a.cpp:1:2: runtime error: shift exponent 64", + }) + code, stdout, _ = self.run_main(junit) + self.assertEqual(code, 1) + self.assertIn("| Failed test | First sanitizer report |\n| - | - |\n" + "| `overread` | SUMMARY: AddressSanitizer: heap-buffer-overflow in A\\|B |\n" + "| `plain` | |\n" + "| `ub` | /a.cpp:1:2: runtime error: shift exponent 64 |\n", stdout) + + def test_failed_tests_without_reports_keep_the_single_column_table(self): + code, stdout, _ = self.run_main(self.write(suite(case("plain", "fail", '<failure message=""/><system-out>FAIL</system-out>')))) + self.assertEqual(code, 1) + self.assertIn("| Failed test |\n| - |\n| `plain` |\n", stdout) + if __name__ == "__main__": unittest.main() From 7af80c966ed6442ae6b2c3893d04e1d01bb4d90f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:13:14 +0000 Subject: [PATCH 325/702] test(agc): run the converted guest memory lifetime test under ASan agc_driver_guest_memory_lifetime_converted runs a relinked fixture that loads the ASan builds of libkernel, libc and libSceAgcDriver, so the ASan runtime is not the first library and ASan stops with 'ASan runtime does not come first in initial library list'. Set verify_asan_link_order=0 for it, as loadexec_arguments already does for its converted host. With it the test passes in an ANYPS5_ENABLE_ASAN build on Linux; other builds are unaffected. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0924d1dd2a..23ed9068fa 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3078,6 +3078,7 @@ if(BUILD_TESTING) $<TARGET_FILE:libSceAgcDriver>) set_tests_properties(agc_driver_guest_memory_lifetime_converted PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_asan_test_options(agc_driver_guest_memory_lifetime_converted verify_asan_link_order=0) endif() set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE) set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF) From c48bc565ba8b62b02ff42d1a8edc961950cfe165 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:23:06 +0000 Subject: [PATCH 326/702] fix(tools): count a test that ctest could not start as failed ctest writes a test whose executable is missing, for example because it failed to link, as status="notrun" with <skipped message="Unable to find executable">, the same shape as a test that skipped itself. sanitizer_report counted both as skipped, which hid a test that did not build. A skipped element now means a skip only when its message is a ctest skip status (SKIP_RETURN_CODE=<n> or SKIP_REGULAR_EXPRESSION_MATCHED, as tools/check_skips.py reads it) or when the status is not a failing one; any other not-run test counts as failed. On a local ASan and UBSan run of main this moves atrac9_gradient, whose executable does not link, from the 721 skipped tests to the failed ones. --- tools/sanitizer_report.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/sanitizer_report.py b/tools/sanitizer_report.py index 593af2eba3..596fe9908c 100644 --- a/tools/sanitizer_report.py +++ b/tools/sanitizer_report.py @@ -6,6 +6,7 @@ from pathlib import Path FAILED_STATUSES = ("fail", "notrun") SKIPPED_STATUSES = ("disabled",) +CTEST_SKIP = re.compile(r"SKIP_RETURN_CODE=\d+|SKIP_REGULAR_EXPRESSION_MATCHED", re.IGNORECASE) SANITIZERS = ("address", "undefined", "thread") REPORT_LINES = ( re.compile(r"SUMMARY: \w+Sanitizer: .*"), @@ -24,9 +25,11 @@ def results(path): name = case.get("name") if not name: raise ValueError(f"{path}: testcase has no name") - if case.find("skipped") is not None or case.get("status") in SKIPPED_STATUSES: + skipped, status = case.find("skipped"), case.get("status") + reason = (skipped.get("message") or "").strip() if skipped is not None else "" + if status in SKIPPED_STATUSES or (skipped is not None and (CTEST_SKIP.fullmatch(reason) or status not in FAILED_STATUSES)): outcome = "skipped" - elif case.get("status") in FAILED_STATUSES or case.find("failure") is not None or case.find("error") is not None: + elif status in FAILED_STATUSES or case.find("failure") is not None or case.find("error") is not None: outcome = "failed" else: outcome = "passed" From f533409117e72ca198761c51027576440cf26386 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:23:33 +0000 Subject: [PATCH 327/702] test(tools): cover ctest's not-run tests in the sanitizer summary Use the shapes ctest writes: a not-run test with SKIP_RETURN_CODE=77 or SKIP_REGULAR_EXPRESSION_MATCHED is a skip, while a not-run test whose executable is missing ('Unable to find executable') or whose skipped element has no message is a failure that fails the run. The test fails with the previous classification. --- tools/tests/test_sanitizer_report.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/tests/test_sanitizer_report.py b/tools/tests/test_sanitizer_report.py index f92a4dc7e7..e9a0a052ed 100644 --- a/tools/tests/test_sanitizer_report.py +++ b/tools/tests/test_sanitizer_report.py @@ -45,6 +45,21 @@ class SanitizerReportTests(unittest.TestCase): "passes": "passed", "fails": "failed", "skips": "skipped", "never_built": "failed", "switched_off": "skipped", }) + def test_not_run_tests_are_skips_only_with_a_ctest_skip_status(self): + path = self.write(suite( + case("skip_code", "notrun", '<skipped message="SKIP_RETURN_CODE=77"/><system-out>skipped, no display</system-out>'), + case("skip_regex", "notrun", '<skipped message="SKIP_REGULAR_EXPRESSION_MATCHED"/>'), + case("missing", "notrun", '<skipped message="Unable to find executable"/>' + '<system-out>Unable to find executable: build/tests/missing_tests</system-out>'), + case("no_message", "notrun", "<skipped/>"), + )) + self.assertEqual(sanitizer_report.results(path), { + "skip_code": "skipped", "skip_regex": "skipped", "missing": "failed", "no_message": "failed", + }) + code, stdout, _ = self.run_main(path) + self.assertEqual(code, 1) + self.assertIn("4 tests: 0 passed, 2 skipped, 2 failed.", stdout) + def test_summary_counts_and_sorts_the_failed_tests(self): text, failing = sanitizer_report.summarize("ASan", {"b": "failed", "a": "failed", "c": "passed", "d": "skipped"}) self.assertTrue(failing) From 6b51a19a243a8ffd0e2eccf6c1d5720c01acaff5 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:23:40 +0000 Subject: [PATCH 328/702] ci: list the known failures of the sanitizer run Add .github/sanitizer-known-failures.txt, one test per line with the sanitizers it fails under and the reason, for the tests that fail in an ASan and UBSan build of main: stack switches that ASan is not told about (fiber, guest_user_context), tests whose setup conflicts with ASan (guest_getrlimit, guest_pthread_stack, guest_backtrace), undefined behavior in third-party code (LibAtrac9, libvorbis), and bugs whose fixes are open pull requests (#9548, #9710, #9711, #10829, #10921) or still to be sent. Each reason quotes what the sanitizer or the test reports on a local run. --- .github/sanitizer-known-failures.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/sanitizer-known-failures.txt diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt new file mode 100644 index 0000000000..ffc0ac1481 --- /dev/null +++ b/.github/sanitizer-known-failures.txt @@ -0,0 +1,18 @@ +agc_driver_graphics undefined The ASan build's CopyGuestBytes in Execution/src/GuestMemory.cpp loads guest words through a misaligned volatile const uint64_t pointer (UBSan alignment check); a fix is pending. +agc_driver_graphics_maps_fallback undefined The ASan build's CopyGuestBytes in Execution/src/GuestMemory.cpp loads guest words through a misaligned volatile const uint64_t pointer (UBSan alignment check); a fix is pending. +agc_driver_pm4 undefined The ASan build's CopyGuestBytes in Execution/src/GuestMemory.cpp loads guest words through a misaligned volatile const uint64_t pointer (UBSan alignment check); a fix is pending. +atrac9_config_data undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. +atrac9_gradient address,undefined atrac9_gradient_tests does not link in an ANYPS5_ENABLE_ASAN build ('undefined reference to symbol pow'), so ctest reports it as not run; #9710 links it with libm. +fiber address ASan reports a stack-buffer-underflow in _sceFiberInitializeWithInternalOptionImpl on fiber stack memory that earlier guest frames left poisoned; ASan is not told about the switches between fiber stacks. +guest_ajm undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. +guest_audiodec undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. +guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. +guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.hpp subtracts 16 from an INT32_MIN keyboard type (signed integer overflow); a fix is pending. +guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). +guest_pthread_stack address Aborts only in ASan builds, when the worker writes its marker 4096 bytes below the top of its own running stack. +guest_sce_v2_exceptions address The test's inspection hook copies 16 bytes of an 8-byte exception object (heap-buffer-overflow); #10829 fixes the test. +guest_system_configuration undefined The sysctl copy-out calls memcpy with a null source for kern.proc.env; #10921 fixes it. +guest_user_context address setcontext switches to a stack ASan does not know; ASan ignores __asan_handle_no_return there and reports a stack-buffer-overflow in setcontext_nid_postfix. +guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. +input_magic address Its large-input case runs the relinker under a 512 MiB RLIMIT_AS, where an ASan relinker cannot start; #9711 bounds the input with ASan's allocation cap instead. +ngs2_atrac9 undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. From fae208569a65adf08a1c2c028f925c0cef36c646 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:24:19 +0000 Subject: [PATCH 329/702] test(tools): check the repository's known sanitizer failures Parse .github/sanitizer-known-failures.txt for every sanitizer and check that its entries stay sorted by test name, so a malformed or misplaced entry fails the sanitizer_report test in the regular build of the pull request that adds it, instead of the next nightly run. Names that no longer match a test are reported by the nightly summary, since many test names are generated in CMake loops. --- tools/tests/test_sanitizer_report.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/tests/test_sanitizer_report.py b/tools/tests/test_sanitizer_report.py index e9a0a052ed..e343265e07 100644 --- a/tools/tests/test_sanitizer_report.py +++ b/tools/tests/test_sanitizer_report.py @@ -8,6 +8,9 @@ import unittest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import sanitizer_report +REPOSITORY = Path(__file__).resolve().parents[2] +KNOWN_FAILURES = REPOSITORY / ".github" / "sanitizer-known-failures.txt" + def case(name, status="run", body=""): return f'<testcase name="{name}" classname="{name}" time="0.1" status="{status}">{body}</testcase>' @@ -205,5 +208,15 @@ class SanitizerReportTests(unittest.TestCase): self.assertIn("| Failed test |\n| - |\n| `plain` |\n", stdout) + +class RepositoryKnownFailuresTests(unittest.TestCase): + def test_every_entry_parses(self): + self.assertTrue(sanitizer_report.load_known(KNOWN_FAILURES, list(sanitizer_report.SANITIZERS))) + + def test_entries_stay_sorted_by_test_name(self): + names = [line.split(None, 1)[0] for line in KNOWN_FAILURES.read_text(encoding="utf-8").splitlines() if line.strip()] + self.assertEqual(names, sorted(names)) + + if __name__ == "__main__": unittest.main() From 1f5d0b347cbc4730626eb64821c0931e6119282e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:24:30 +0000 Subject: [PATCH 330/702] ci: run every test under ASan and UBSan nightly Add the Sanitizers workflow. Every night at 03:41 UTC, and on workflow_dispatch, it builds the full runtime on ubuntu-latest with -DANYPS5_ENABLE_ASAN=ON and -fsanitize=undefined -fno-sanitize=vptr, building only the F16ToF32Unpack, F64FmaSubnormals and RegistrationPrepareShutdown AGC execution tests like the Windows MSYS2 workflow, and runs every registered test with ASAN_OPTIONS=detect_leaks=0: and UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1. ASAN_OPTIONS ends with ':' because tests append their own options with string_append. The build keeps going after a failing target, so its tests report as not run instead of hiding the rest. tools/sanitizer_report.py then writes the job summary from ctest's JUnit file and fails the job only for failures missing from .github/sanitizer-known-failures.txt. It reuses the submodule, ccache and FFmpeg caching of the Windows MSYS2 workflow, frees preinstalled toolchains the build does not use to make room for the instrumented objects, and uploads ctest.log and ctest.xml. --- .github/workflows/sanitizers.yml | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 .github/workflows/sanitizers.yml diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 0000000000..1fa69d2816 --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,108 @@ +name: Sanitizers + +on: + schedule: + - cron: '41 3 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: sanitizers-${{ github.ref }} + cancel-in-progress: true + +env: + CONFIGURE: >- + -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + -DANYPS5_ENABLE_ASAN=ON + SANITIZER_FLAGS: -g1 -fsanitize=undefined -fno-sanitize=vptr + +jobs: + address-undefined: + runs-on: ubuntu-latest + timeout-minutes: 330 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + - id: submodules + name: Key the submodules on their commits + run: | + echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT" + - id: submodule-cache + uses: actions/cache/restore@v6 + with: + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} + - name: Check out the submodules + run: | + if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi + git submodule update --init --recursive --depth 1 --jobs 8 + - if: steps.submodule-cache.outputs.cache-hit != 'true' + run: tar -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules + - if: steps.submodule-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} + - name: Install the build and test dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends ninja-build libx11-dev libxext-dev libasound2-dev libpulse-dev mesa-vulkan-drivers + - name: Free disk space for the sanitizer build + run: | + df -h / + sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /opt/hostedtoolcache/CodeQL + df -h / + - uses: hendrikmuhs/ccache-action@v1.2.24 + with: + key: sanitizers + max-size: 2G + save: ${{ github.ref == 'refs/heads/main' }} + evict-old-files: job + - id: ffmpeg-cache + uses: actions/cache/restore@v6 + with: + path: build/externals + key: ffmpeg-sanitizers-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }} + - run: cmake -S . -B build $CONFIGURE "-DCMAKE_C_FLAGS=$SANITIZER_FLAGS" "-DCMAKE_CXX_FLAGS=$SANITIZER_FLAGS" "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown" + - if: steps.ffmpeg-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: build/externals + key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }} + - name: Check that every test executable is registered with ctest + run: python3 tools/check_test_registration.py build + - name: Build, keeping on after a target fails so its tests report as not run + continue-on-error: true + run: cmake --build build -- -k 0 + - continue-on-error: true + run: cmake --build build --target libs -- -k 0 + - name: Run every test under ASan and UBSan + if: ${{ !cancelled() }} + working-directory: build + env: + ASAN_OPTIONS: 'detect_leaks=0:' + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 + run: ctest --output-on-failure --timeout 300 --parallel 4 --output-log ctest.log --output-junit ctest.xml || true + - name: Summarize the sanitizer findings + if: ${{ !cancelled() }} + run: >- + python3 tools/sanitizer_report.py build/ctest.xml --title "ASan and UBSan" + --known .github/sanitizer-known-failures.txt --sanitizer address --sanitizer undefined + --summary "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@v7 + if: ${{ !cancelled() }} + with: + name: sanitizer-tests + path: | + build/ctest.log + build/ctest.xml + if-no-files-found: ignore From 448275eff5046ac27ea87fdd10c232b8e50abfca Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 06:34:57 +0000 Subject: [PATCH 331/702] docs(libSceAudioOut): list sceAudioOut2ContextSetAttributes as a silent stub Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAudioOut.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 7946f949d4..3d22e3c43b 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -5,6 +5,7 @@ - [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 13.60 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are - [sceAudioOutMasteringTerm](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - returns 0 without doing anything: there is no mastering stage to tear down, and `sceAudioOutMasteringInit` keeps no state. Whether the console fails a Term without a matching Init is unknown - [sceAudioOutSetMainOutput](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - returns 0 and does nothing, as the console function does on firmware 12.70: [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L25) reads that export as `xor eax,eax; ret`. The export reads no argument, so its parameters stay unknown; other firmware versions are unverified +- [sceAudioOut2ContextSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - returns 0 and ignores every attribute; what a context attribute changes on a console is unknown. PPSA25286 01.000.568 sets attribute 2 to a 4-byte 1 at start-up, and PPSA02225 passes two attributes from an object at its single call. ### Unknown function info From c1fa1e58d31c34098e26abf6a38f57af0d45ea3a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:24:46 +0000 Subject: [PATCH 332/702] docs(dev): describe the nightly sanitizer workflow Document in BUILD.md, next to -DANYPS5_ENABLE_ASAN, what the Sanitizers workflow builds and runs, where the known failures are listed and what the job summary reports, and how to repeat a run locally: end ASAN_OPTIONS with ':' because tests append their own options, and set UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 so undefined behavior fails the test. --- docs/dev/BUILD.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 8bb6b25b00..d1a5647772 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -122,6 +122,8 @@ Project switches accept `ON` or `OFF`: GCC and Clang compile libc with `-ffp-contract=off` whatever the target CPU. Toolchains that default to x86-64-v3, such as GCC 15 on Ubuntu 26.04, would otherwise fuse `a * b + c` into FMA instructions, which changes the last bit of math results that must match the console. +The [Sanitizers workflow](../../.github/workflows/sanitizers.yml) builds the full runtime on Ubuntu with `-DANYPS5_ENABLE_ASAN=ON` and UBSan (`-fsanitize=undefined -fno-sanitize=vptr`) nightly at 03:41 UTC, runs every test and can be started manually with `workflow_dispatch`. It builds only the `F16ToF32Unpack`, `F64FmaSubnormals` and `RegistrationPrepareShutdown` AGC execution tests, like the Windows MSYS2 workflow. A test that fails because of the sanitizers themselves, or because of a bug whose fix is still open, is listed with the reason in [sanitizer-known-failures.txt](../../.github/sanitizer-known-failures.txt), one test per line with its sanitizers and the reason,. The job summary, written by `tools/sanitizer_report.py`, separates unexpected failures from known ones and names listed tests that passed or did not run, so their entries can be removed. To repeat a run locally, end `ASAN_OPTIONS` with `:` (for example `ASAN_OPTIONS=detect_leaks=0:`), because tests append their own options to it, and set `UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1` so that undefined behavior fails the test. + Build configuration parameters: | Flag | Value | From 43067fef60bbe324918aad81b0516050a52daeb7 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:38:10 +0000 Subject: [PATCH 333/702] ci: install libxrandr-dev for the sanitizer build The bundled SDL configuration stops without XRandR ('SDL has no XRandR support, so X11 display refresh rates cannot be queried'), as docs/dev/BUILD.md notes, and ubuntu-latest does not ship libxrandr-dev. Found by a run of the workflow on ubuntu-latest. --- .github/workflows/sanitizers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index 1fa69d2816..a026bc1cc3 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -54,7 +54,7 @@ jobs: - name: Install the build and test dependencies run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends ninja-build libx11-dev libxext-dev libasound2-dev libpulse-dev mesa-vulkan-drivers + sudo apt-get install -y --no-install-recommends ninja-build libx11-dev libxext-dev libxrandr-dev libasound2-dev libpulse-dev mesa-vulkan-drivers - name: Free disk space for the sanitizer build run: | df -h / From 793c0ba05de99928c742d4750df6d0d9b89d0daf Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:38:33 +0000 Subject: [PATCH 334/702] ci: build and test in the sanitizer workflow only after configuration succeeds When configuration failed, the test and summary steps still ran on the half-configured build directory, found no tests and reported success, so the failed run looked partly green. Gate the build, test and summary steps on the configure step; the job then fails at configuration alone. The build steps keep continue-on-error, so a target that fails to build still shows up as not-run tests. --- .github/workflows/sanitizers.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index a026bc1cc3..f70e8607f4 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -71,7 +71,8 @@ jobs: with: path: build/externals key: ffmpeg-sanitizers-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }} - - run: cmake -S . -B build $CONFIGURE "-DCMAKE_C_FLAGS=$SANITIZER_FLAGS" "-DCMAKE_CXX_FLAGS=$SANITIZER_FLAGS" "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown" + - id: configure + run: cmake -S . -B build $CONFIGURE "-DCMAKE_C_FLAGS=$SANITIZER_FLAGS" "-DCMAKE_CXX_FLAGS=$SANITIZER_FLAGS" "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown" - if: steps.ffmpeg-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v6 continue-on-error: true @@ -81,19 +82,21 @@ jobs: - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - name: Build, keeping on after a target fails so its tests report as not run + if: ${{ !cancelled() && steps.configure.outcome == 'success' }} continue-on-error: true run: cmake --build build -- -k 0 - - continue-on-error: true + - if: ${{ !cancelled() && steps.configure.outcome == 'success' }} + continue-on-error: true run: cmake --build build --target libs -- -k 0 - name: Run every test under ASan and UBSan - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.configure.outcome == 'success' }} working-directory: build env: ASAN_OPTIONS: 'detect_leaks=0:' UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 run: ctest --output-on-failure --timeout 300 --parallel 4 --output-log ctest.log --output-junit ctest.xml || true - name: Summarize the sanitizer findings - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.configure.outcome == 'success' }} run: >- python3 tools/sanitizer_report.py build/ctest.xml --title "ASan and UBSan" --known .github/sanitizer-known-failures.txt --sanitizer address --sanitizer undefined From e6194c7f4b65d1ec3c63350321bf1765ea1c72f5 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:38:41 +0000 Subject: [PATCH 335/702] fix(tools): reject a sanitizer report without tests A JUnit file without testcases, as ctest writes when it finds no tests in a build directory, gave '0 tests' and exit code 0, so a run that built nothing passed. Treat it as an unreadable report, with exit code 2. --- tools/sanitizer_report.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/sanitizer_report.py b/tools/sanitizer_report.py index 596fe9908c..9c98bcb9e0 100644 --- a/tools/sanitizer_report.py +++ b/tools/sanitizer_report.py @@ -34,6 +34,8 @@ def results(path): else: outcome = "passed" found[name] = outcome + if not found: + raise ValueError(f"{path}: the report lists no tests") return found From 03ed97c6fb75b4ed40879a2a28ebc4ddabcff735 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:38:55 +0000 Subject: [PATCH 336/702] test(tools): cover a sanitizer report without tests Check that an empty testsuite and an empty testsuites file both exit with 2, print nothing and leave the --summary file untouched. The test fails with the previous behavior, which reported 0 tests and exit code 0. --- tools/tests/test_sanitizer_report.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/tests/test_sanitizer_report.py b/tools/tests/test_sanitizer_report.py index e343265e07..e37e8328ca 100644 --- a/tools/tests/test_sanitizer_report.py +++ b/tools/tests/test_sanitizer_report.py @@ -90,6 +90,16 @@ class SanitizerReportTests(unittest.TestCase): code, _, stderr = self.run_main(Path(self.directory.name) / "missing.xml") self.assertEqual(code, 2) + def test_a_report_without_tests_is_rejected(self): + summary = self.write("", "summary.md") + for text in (suite(), '<testsuites><testsuite name="(empty)" tests="0"/></testsuites>'): + with self.subTest(text=text): + code, stdout, stderr = self.run_main(self.write(text), "--summary", summary) + self.assertEqual(code, 2) + self.assertEqual(stdout, "") + self.assertIn("lists no tests", stderr) + self.assertEqual(summary.read_text(encoding="utf-8"), "") + def test_summary_file_is_appended_to(self): summary = self.write("existing\n", "summary.md") code, stdout, _ = self.run_main(self.write(suite(case("passes"))), "--title", "Nightly", "--summary", summary) From 1b6abfe41a1e8b5378b09924bd2ee62706cd303e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:44:29 +0000 Subject: [PATCH 337/702] ci: name the pull requests that fix known sanitizer failures Point the known failures of agc_driver_pm4, agc_driver_graphics, agc_driver_graphics_maps_fallback and guest_convert_keycode at the pull requests that fix them, #10973 and #10968. With the misaligned loads fixed, agc_driver_graphics and agc_driver_graphics_maps_fallback stop on a second report, a TextureDetiler reference bound to a null pointer in Graphics/src/DepthSurface.cpp:730, so their reason names both. --- .github/sanitizer-known-failures.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index ffc0ac1481..206b2c07a4 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -1,13 +1,13 @@ -agc_driver_graphics undefined The ASan build's CopyGuestBytes in Execution/src/GuestMemory.cpp loads guest words through a misaligned volatile const uint64_t pointer (UBSan alignment check); a fix is pending. -agc_driver_graphics_maps_fallback undefined The ASan build's CopyGuestBytes in Execution/src/GuestMemory.cpp loads guest words through a misaligned volatile const uint64_t pointer (UBSan alignment check); a fix is pending. -agc_driver_pm4 undefined The ASan build's CopyGuestBytes in Execution/src/GuestMemory.cpp loads guest words through a misaligned volatile const uint64_t pointer (UBSan alignment check); a fix is pending. +agc_driver_graphics undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler. +agc_driver_graphics_maps_fallback undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler. +agc_driver_pm4 undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp); #10973 fixes them. atrac9_config_data undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. atrac9_gradient address,undefined atrac9_gradient_tests does not link in an ANYPS5_ENABLE_ASAN build ('undefined reference to symbol pow'), so ctest reports it as not run; #9710 links it with libm. fiber address ASan reports a stack-buffer-underflow in _sceFiberInitializeWithInternalOptionImpl on fiber stack memory that earlier guest frames left poisoned; ASan is not told about the switches between fiber stacks. guest_ajm undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. guest_audiodec undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. -guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.hpp subtracts 16 from an INT32_MIN keyboard type (signed integer overflow); a fix is pending. +guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.hpp subtracts 16 from an INT32_MIN keyboard type (signed integer overflow); #10968 fixes it. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_pthread_stack address Aborts only in ASan builds, when the worker writes its marker 4096 bytes below the top of its own running stack. guest_sce_v2_exceptions address The test's inspection hook copies 16 bytes of an 8-byte exception object (heap-buffer-overflow); #10829 fixes the test. From 85a62536c1a9c3a2d6ecf963bd8a0f1286d41999 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:41:40 +0000 Subject: [PATCH 338/702] ci: drop the trailing colon from the sanitizer run's ASAN_OPTIONS The workflow and BUILD.md said ASAN_OPTIONS must end with ':' because tests append their own options with string_append. That is wrong: add_asan_test_options appends ':<options>', so ASAN_OPTIONS=detect_leaks=0 already gives detect_leaks=0:detect_stack_use_after_return=0 for the fiber test. Use detect_leaks=0 and remove the claim. --- .github/workflows/sanitizers.yml | 2 +- docs/dev/BUILD.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index f70e8607f4..914ad341bf 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -92,7 +92,7 @@ jobs: if: ${{ !cancelled() && steps.configure.outcome == 'success' }} working-directory: build env: - ASAN_OPTIONS: 'detect_leaks=0:' + ASAN_OPTIONS: detect_leaks=0 UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 run: ctest --output-on-failure --timeout 300 --parallel 4 --output-log ctest.log --output-junit ctest.xml || true - name: Summarize the sanitizer findings diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index d1a5647772..7289646479 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -122,7 +122,7 @@ Project switches accept `ON` or `OFF`: GCC and Clang compile libc with `-ffp-contract=off` whatever the target CPU. Toolchains that default to x86-64-v3, such as GCC 15 on Ubuntu 26.04, would otherwise fuse `a * b + c` into FMA instructions, which changes the last bit of math results that must match the console. -The [Sanitizers workflow](../../.github/workflows/sanitizers.yml) builds the full runtime on Ubuntu with `-DANYPS5_ENABLE_ASAN=ON` and UBSan (`-fsanitize=undefined -fno-sanitize=vptr`) nightly at 03:41 UTC, runs every test and can be started manually with `workflow_dispatch`. It builds only the `F16ToF32Unpack`, `F64FmaSubnormals` and `RegistrationPrepareShutdown` AGC execution tests, like the Windows MSYS2 workflow. A test that fails because of the sanitizers themselves, or because of a bug whose fix is still open, is listed with the reason in [sanitizer-known-failures.txt](../../.github/sanitizer-known-failures.txt), one test per line with its sanitizers and the reason,. The job summary, written by `tools/sanitizer_report.py`, separates unexpected failures from known ones and names listed tests that passed or did not run, so their entries can be removed. To repeat a run locally, end `ASAN_OPTIONS` with `:` (for example `ASAN_OPTIONS=detect_leaks=0:`), because tests append their own options to it, and set `UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1` so that undefined behavior fails the test. +The [Sanitizers workflow](../../.github/workflows/sanitizers.yml) builds the full runtime on Ubuntu with `-DANYPS5_ENABLE_ASAN=ON` and UBSan (`-fsanitize=undefined -fno-sanitize=vptr`) nightly at 03:41 UTC, runs every test and can be started manually with `workflow_dispatch`. It builds only the `F16ToF32Unpack`, `F64FmaSubnormals` and `RegistrationPrepareShutdown` AGC execution tests, like the Windows MSYS2 workflow. A test that fails because of the sanitizers themselves, or because of a bug whose fix is still open, is listed with the reason in [sanitizer-known-failures.txt](../../.github/sanitizer-known-failures.txt), one test per line with its sanitizers and the reason,. The job summary, written by `tools/sanitizer_report.py`, separates unexpected failures from known ones and names listed tests that passed or did not run, so their entries can be removed. To repeat a run locally, set `ASAN_OPTIONS=detect_leaks=0` and `UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1`, so that undefined behavior fails the test. Build configuration parameters: From 5c76b7335cc4e15ff68542cfe7dcb0d501a30097 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:08:38 +0000 Subject: [PATCH 339/702] ci(sanitizers): drop the known failures that pass on ubuntu-latest --- .github/sanitizer-known-failures.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 206b2c07a4..61ea211d5c 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -3,16 +3,13 @@ agc_driver_graphics_maps_fallback undefined UBSan stops on the misaligned guest agc_driver_pm4 undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp); #10973 fixes them. atrac9_config_data undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. atrac9_gradient address,undefined atrac9_gradient_tests does not link in an ANYPS5_ENABLE_ASAN build ('undefined reference to symbol pow'), so ctest reports it as not run; #9710 links it with libm. -fiber address ASan reports a stack-buffer-underflow in _sceFiberInitializeWithInternalOptionImpl on fiber stack memory that earlier guest frames left poisoned; ASan is not told about the switches between fiber stacks. guest_ajm undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. guest_audiodec undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.hpp subtracts 16 from an INT32_MIN keyboard type (signed integer overflow); #10968 fixes it. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). -guest_pthread_stack address Aborts only in ASan builds, when the worker writes its marker 4096 bytes below the top of its own running stack. guest_sce_v2_exceptions address The test's inspection hook copies 16 bytes of an 8-byte exception object (heap-buffer-overflow); #10829 fixes the test. guest_system_configuration undefined The sysctl copy-out calls memcpy with a null source for kern.proc.env; #10921 fixes it. -guest_user_context address setcontext switches to a stack ASan does not know; ASan ignores __asan_handle_no_return there and reports a stack-buffer-overflow in setcontext_nid_postfix. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. input_magic address Its large-input case runs the relinker under a 512 MiB RLIMIT_AS, where an ASan relinker cannot start; #9711 bounds the input with ASan's allocation cap instead. ngs2_atrac9 undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. From 3f16530cb6e57284a06ad189a0a4f9ad443b4d9c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:08:38 +0000 Subject: [PATCH 340/702] ci(sanitizers): list agc_performance_overlay's sanitizer timeout as known --- .github/sanitizer-known-failures.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 61ea211d5c..5f5ef6d74f 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -1,6 +1,7 @@ agc_driver_graphics undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler. agc_driver_graphics_maps_fallback undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler. agc_driver_pm4 undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp); #10973 fixes them. +agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in the sanitizer build on ubuntu-latest (timed out at 30.18 s); it passes without sanitizers. atrac9_config_data undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. atrac9_gradient address,undefined atrac9_gradient_tests does not link in an ANYPS5_ENABLE_ASAN build ('undefined reference to symbol pow'), so ctest reports it as not run; #9710 links it with libm. guest_ajm undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. From 26d9e2d95be2865dc5896de58699f3540cb5953b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:13:56 +0000 Subject: [PATCH 341/702] ci(sanitizers): list guest_math's FMA contraction failure as known --- .github/sanitizer-known-failures.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 5f5ef6d74f..21d3d9206c 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -9,6 +9,7 @@ guest_audiodec undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3 guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.hpp subtracts 16 from an INT32_MIN keyboard type (signed integer overflow); #10968 fixes it. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). +guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. guest_sce_v2_exceptions address The test's inspection hook copies 16 bytes of an 8-byte exception object (heap-buffer-overflow); #10829 fixes the test. guest_system_configuration undefined The sysctl copy-out calls memcpy with a null source for kern.proc.env; #10921 fixes it. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. From 5ed54879f7e4bae1b5c93bd55131bfe6f92530ab Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:19:32 +0000 Subject: [PATCH 342/702] ci(sanitizers): drop the LibAtrac9 shift failures that #9548 fixed --- .github/sanitizer-known-failures.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 21d3d9206c..a1af429f06 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -2,10 +2,7 @@ agc_driver_graphics undefined UBSan stops on the misaligned guest word loads of agc_driver_graphics_maps_fallback undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler. agc_driver_pm4 undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp); #10973 fixes them. agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in the sanitizer build on ubuntu-latest (timed out at 30.18 s); it passes without sanitizers. -atrac9_config_data undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. atrac9_gradient address,undefined atrac9_gradient_tests does not link in an ANYPS5_ENABLE_ASAN build ('undefined reference to symbol pow'), so ctest reports it as not run; #9710 links it with libm. -guest_ajm undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. -guest_audiodec undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.hpp subtracts 16 from an INT32_MIN keyboard type (signed integer overflow); #10968 fixes it. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). @@ -14,4 +11,3 @@ guest_sce_v2_exceptions address The test's inspection hook copies 16 bytes of an guest_system_configuration undefined The sysctl copy-out calls memcpy with a null source for kern.proc.env; #10921 fixes it. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. input_magic address Its large-input case runs the relinker under a 512 MiB RLIMIT_AS, where an ASan relinker cannot start; #9711 bounds the input with ASan's allocation cap instead. -ngs2_atrac9 undefined LibAtrac9's BitReverse32 shifts a 32-bit value by 32 (3rdparty/LibAtrac9/C/src/utility.c:14); #9548 guards the shift. From a84817ebb0954040a28f1fda9e919446c093b58a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:27:54 +0000 Subject: [PATCH 343/702] ci(sanitizers): list guest_posix_spawn's SIGBUS child as a known ASan failure --- .github/sanitizer-known-failures.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index a1af429f06..4c474fbca6 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -7,6 +7,7 @@ guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.hpp subtracts 16 from an INT32_MIN keyboard type (signed integer overflow); #10968 fixes it. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. +guest_posix_spawn address The child raises SIGBUS on purpose to test waitpid, but ASan handles SIGBUS in it and aborts instead; #11239 sets handle_sigbus=0 for this test. guest_sce_v2_exceptions address The test's inspection hook copies 16 bytes of an 8-byte exception object (heap-buffer-overflow); #10829 fixes the test. guest_system_configuration undefined The sysctl copy-out calls memcpy with a null source for kern.proc.env; #10921 fixes it. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. From 6d2d3c72320290983be66d9147a23b210856f547 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:36:25 +0000 Subject: [PATCH 344/702] ci(sanitizers): drop atrac9_gradient's link failure that #9710 fixed --- .github/sanitizer-known-failures.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 4c474fbca6..0c07f94f6c 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -2,7 +2,6 @@ agc_driver_graphics undefined UBSan stops on the misaligned guest word loads of agc_driver_graphics_maps_fallback undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler. agc_driver_pm4 undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp); #10973 fixes them. agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in the sanitizer build on ubuntu-latest (timed out at 30.18 s); it passes without sanitizers. -atrac9_gradient address,undefined atrac9_gradient_tests does not link in an ANYPS5_ENABLE_ASAN build ('undefined reference to symbol pow'), so ctest reports it as not run; #9710 links it with libm. guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.hpp subtracts 16 from an INT32_MIN keyboard type (signed integer overflow); #10968 fixes it. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). From 52952dec6f03ab858ad434e32afe479269196926 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:38:40 +0000 Subject: [PATCH 345/702] ci(sanitizers): drop the guest_sce_v2_exceptions failure that #10829 fixed --- .github/sanitizer-known-failures.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 0c07f94f6c..c51713d9fa 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -7,7 +7,6 @@ guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.h guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. guest_posix_spawn address The child raises SIGBUS on purpose to test waitpid, but ASan handles SIGBUS in it and aborts instead; #11239 sets handle_sigbus=0 for this test. -guest_sce_v2_exceptions address The test's inspection hook copies 16 bytes of an 8-byte exception object (heap-buffer-overflow); #10829 fixes the test. guest_system_configuration undefined The sysctl copy-out calls memcpy with a null source for kern.proc.env; #10921 fixes it. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. input_magic address Its large-input case runs the relinker under a 512 MiB RLIMIT_AS, where an ASan relinker cannot start; #9711 bounds the input with ASan's allocation cap instead. From 31833c43f1596812a5b55c7226f1c647141b95af Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:38:40 +0000 Subject: [PATCH 346/702] ci(sanitizers): drop the guest_convert_keycode failure that #10968 fixed --- .github/sanitizer-known-failures.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index c51713d9fa..5199c41c99 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -3,7 +3,6 @@ agc_driver_graphics_maps_fallback undefined UBSan stops on the misaligned guest agc_driver_pm4 undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp); #10973 fixes them. agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in the sanitizer build on ubuntu-latest (timed out at 30.18 s); it passes without sanitizers. guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. -guest_convert_keycode undefined LookUp in libSceConvertKeycode/ImeKeyboardType.hpp subtracts 16 from an INT32_MIN keyboard type (signed integer overflow); #10968 fixes it. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. guest_posix_spawn address The child raises SIGBUS on purpose to test waitpid, but ASan handles SIGBUS in it and aborts instead; #11239 sets handle_sigbus=0 for this test. From 96cd705cfdf6dea3089b971eae44431e068e4ee2 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:38:40 +0000 Subject: [PATCH 347/702] ci(sanitizers): drop the guest_posix_spawn failure that #11239 fixed --- .github/sanitizer-known-failures.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 5199c41c99..5fae576359 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -5,7 +5,6 @@ agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in th guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. -guest_posix_spawn address The child raises SIGBUS on purpose to test waitpid, but ASan handles SIGBUS in it and aborts instead; #11239 sets handle_sigbus=0 for this test. guest_system_configuration undefined The sysctl copy-out calls memcpy with a null source for kern.proc.env; #10921 fixes it. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. input_magic address Its large-input case runs the relinker under a 512 MiB RLIMIT_AS, where an ASan relinker cannot start; #9711 bounds the input with ASan's allocation cap instead. From 307f9f65228430de17243f99b13495b811fbf29a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:38:41 +0000 Subject: [PATCH 348/702] ci(sanitizers): drop the guest_system_configuration failure that #10921 fixed --- .github/sanitizer-known-failures.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 5fae576359..f5ced0e893 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -5,6 +5,5 @@ agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in th guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. -guest_system_configuration undefined The sysctl copy-out calls memcpy with a null source for kern.proc.env; #10921 fixes it. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. input_magic address Its large-input case runs the relinker under a 512 MiB RLIMIT_AS, where an ASan relinker cannot start; #9711 bounds the input with ASan's allocation cap instead. From 4b0061f9c1af3c6a585ad6af1c9da30c9aa03844 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:38:41 +0000 Subject: [PATCH 349/702] ci(sanitizers): drop the input_magic failure that #9711 fixed --- .github/sanitizer-known-failures.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index f5ced0e893..9e9706919a 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -6,4 +6,3 @@ guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. -input_magic address Its large-input case runs the relinker under a 512 MiB RLIMIT_AS, where an ASan relinker cannot start; #9711 bounds the input with ASan's allocation cap instead. From a34c0378eab3244369e2902ca53569d49b72fb2a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:06:32 +0000 Subject: [PATCH 350/702] ci(sanitizers): name the libvorbis update for guest_vorbis_dec --- .github/sanitizer-known-failures.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 9e9706919a..1aa5299437 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -5,4 +5,4 @@ agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in th guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. -guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); third-party code. +guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); #11480 updates libvorbis to the upstream fix. From 241a0ec1150d5b38008da04cec4644dd422b43fc Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:06:32 +0000 Subject: [PATCH 351/702] ci(sanitizers): name the timeout change for agc_performance_overlay --- .github/sanitizer-known-failures.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 1aa5299437..3bdf080dfe 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -1,7 +1,7 @@ agc_driver_graphics undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler. agc_driver_graphics_maps_fallback undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler. agc_driver_pm4 undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp); #10973 fixes them. -agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in the sanitizer build on ubuntu-latest (timed out at 30.18 s); it passes without sanitizers. +agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in the sanitizer build on ubuntu-latest (timed out at 30.18 s); it passes without sanitizers, and alone it takes about 19 s; #11555 gives it 120 s in ASan builds. guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. From 52809115519ff59eceb5080b31433e67c9a201bb Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:06:32 +0000 Subject: [PATCH 352/702] ci(sanitizers): name the fix for guest_backtrace --- .github/sanitizer-known-failures.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 3bdf080dfe..0d7883838c 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -2,7 +2,7 @@ agc_driver_graphics undefined UBSan stops on the misaligned guest word loads of agc_driver_graphics_maps_fallback undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler. agc_driver_pm4 undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp); #10973 fixes them. agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in the sanitizer build on ubuntu-latest (timed out at 30.18 s); it passes without sanitizers, and alone it takes about 19 s; #11555 gives it 120 s in ASan builds. -guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module'. +guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module', because the sanitizer adds a frame to backtrace(); #9240 starts the frames at the caller. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); #11480 updates libvorbis to the upstream fix. From 0d12e16409f665b3c0a70e5a51f3c34b14e6c096 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 19:38:22 +1300 Subject: [PATCH 353/702] fix(libSceFreeTypeOl): drop the exports main already provides Remove the stubs whose NIDs main now defines. Main's own debt entries stay unchanged; the imports are appended after them. --- core/libs/prx/libSceFreeTypeOl/Export.cpp | 130 ---------------------- docs/dev/debt/libSceFreeTypeOl.md | 2 +- 2 files changed, 1 insertion(+), 131 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOl/Export.cpp b/core/libs/prx/libSceFreeTypeOl/Export.cpp index 5a7e47a493..e01fa51b09 100644 --- a/core/libs/prx/libSceFreeTypeOl/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOl/Export.cpp @@ -420,11 +420,6 @@ int APS5_VABI sceFreeTypeOlUnknown_Ns9a_LjOcDw_nid_no_patch() { } APS5_EXPORT("Ns9a-LjOcDw", sceFreeTypeOlUnknown_Ns9a_LjOcDw_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_EBvr2CZnjVo_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("EBvr2CZnjVo", sceFreeTypeOlUnknown_EBvr2CZnjVo_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_pSgOzwkzx4Y_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -440,11 +435,6 @@ int APS5_VABI sceFreeTypeOlUnknown_Uw_BZJpflxk_nid_no_patch() { } APS5_EXPORT("Uw+BZJpflxk", sceFreeTypeOlUnknown_Uw_BZJpflxk_nid_no_patch); -int APS5_VABI FT_Angle_Diff_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("D1l8+Bu+-Y8", FT_Angle_Diff_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_p90G4PB_rPc_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -460,31 +450,11 @@ int APS5_VABI sceFreeTypeOlUnknown_ZqvTX8u1bEc_nid_no_patch() { } APS5_EXPORT("ZqvTX8u1bEc", sceFreeTypeOlUnknown_ZqvTX8u1bEc_nid_no_patch); -int APS5_VABI FT_Bitmap_Convert_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("cR-yQldyXvs", FT_Bitmap_Convert_nid_no_patch); - -int APS5_VABI FT_Bitmap_Copy_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("1GL9pCFuiHQ", FT_Bitmap_Copy_nid_no_patch); - -int APS5_VABI FT_Bitmap_Done_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("Mu2upxmY2C8", FT_Bitmap_Done_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_wcMdf_kMx7o_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("wcMdf-kMx7o", sceFreeTypeOlUnknown_wcMdf_kMx7o_nid_no_patch); -int APS5_VABI FT_Bitmap_New_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("-AeE58GkM+Q", FT_Bitmap_New_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_h0n4Fb9j2R8_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -510,11 +480,6 @@ int APS5_VABI sceFreeTypeOlUnknown_pO0sA7m_UKY_nid_no_patch() { } APS5_EXPORT("pO0sA7m+UKY", sceFreeTypeOlUnknown_pO0sA7m_UKY_nid_no_patch); -int APS5_VABI FT_Cos_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("yD4-QaNSink", FT_Cos_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_fp_jO_c8Rfk_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -540,11 +505,6 @@ int APS5_VABI sceFreeTypeOlUnknown_UTumVjeIQtg_nid_no_patch() { } APS5_EXPORT("UTumVjeIQtg", sceFreeTypeOlUnknown_UTumVjeIQtg_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown__2rnxRCeHMs_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("-2rnxRCeHMs", sceFreeTypeOlUnknown__2rnxRCeHMs_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown__G9vZcqBAfY_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -600,61 +560,11 @@ int APS5_VABI sceFreeTypeOlUnknown_l55oKq6zmrA_nid_no_patch() { } APS5_EXPORT("l55oKq6zmrA", sceFreeTypeOlUnknown_l55oKq6zmrA_nid_no_patch); -int APS5_VABI FT_Get_CID_From_Glyph_Index_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("NLwiYXdX5WU", FT_Get_CID_From_Glyph_Index_nid_no_patch); - -int APS5_VABI FT_Get_CID_Is_Internally_CID_Keyed_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("p-LFZYbDPDU", FT_Get_CID_Is_Internally_CID_Keyed_nid_no_patch); - -int APS5_VABI FT_Get_CID_Registry_Ordering_Supplement_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("Pr3eI2uGgvU", FT_Get_CID_Registry_Ordering_Supplement_nid_no_patch); - -int APS5_VABI FT_Get_CMap_Format_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("MXgrzq0nh1Q", FT_Get_CMap_Format_nid_no_patch); - -int APS5_VABI FT_Get_CMap_Language_ID_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("2UZg+FAym4c", FT_Get_CMap_Language_ID_nid_no_patch); - -int APS5_VABI FT_Get_Charmap_Index_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("edmDMsPE7Zg", FT_Get_Charmap_Index_nid_no_patch); - -int APS5_VABI FT_Get_FSType_Flags_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("H4eyE4ez7O4", FT_Get_FSType_Flags_nid_no_patch); - -int APS5_VABI FT_Get_Gasp_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("l4azYkX86Ds", FT_Get_Gasp_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_eMKHL_Cj50k_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("eMKHL+Cj50k", sceFreeTypeOlUnknown_eMKHL_Cj50k_nid_no_patch); -int APS5_VABI FT_Get_Glyph_Name_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("tGmYdLIYT98", FT_Get_Glyph_Name_nid_no_patch); - -int APS5_VABI FT_Get_Kerning_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("LPtlB241JSg", FT_Get_Kerning_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_qJFh32pZAJQ_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -675,26 +585,6 @@ int APS5_VABI sceFreeTypeOlUnknown_pORpGtdlfqQ_nid_no_patch() { } APS5_EXPORT("pORpGtdlfqQ", sceFreeTypeOlUnknown_pORpGtdlfqQ_nid_no_patch); -int APS5_VABI FT_Get_Name_Index_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("sZzliEQrICM", FT_Get_Name_Index_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_NVuvsPgTXto_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("NVuvsPgTXto", sceFreeTypeOlUnknown_NVuvsPgTXto_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_qBohKqdkAqE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("qBohKqdkAqE", sceFreeTypeOlUnknown_qBohKqdkAqE_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_OqaxEkRhr_8_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("OqaxEkRhr+8", sceFreeTypeOlUnknown_OqaxEkRhr_8_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_JQyLCevN3aM_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -710,26 +600,11 @@ int APS5_VABI sceFreeTypeOlUnknown_Ik17GQynFpw_nid_no_patch() { } APS5_EXPORT("Ik17GQynFpw", sceFreeTypeOlUnknown_Ik17GQynFpw_nid_no_patch); -int APS5_VABI FT_Get_Postscript_Name_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("GVnNjpJWCXM", FT_Get_Postscript_Name_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_1wFNGIk3FR0_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("1wFNGIk3FR0", sceFreeTypeOlUnknown_1wFNGIk3FR0_nid_no_patch); -int APS5_VABI FT_Get_Sfnt_Name_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("B4-zPp0EATE", FT_Get_Sfnt_Name_nid_no_patch); - -int APS5_VABI FT_Get_Sfnt_Name_Count_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("fbRv4M+XzL8", FT_Get_Sfnt_Name_Count_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_omDz0x8lyHk_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -750,9 +625,4 @@ int APS5_VABI sceFreeTypeOlUnknown_eSFTUAm1wf8_nid_no_patch() { } APS5_EXPORT("eSFTUAm1wf8", sceFreeTypeOlUnknown_eSFTUAm1wf8_nid_no_patch); -int APS5_VABI FT_Get_X11_Font_Format_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("aWMUKhiwUbk", FT_Get_X11_Font_Format_nid_no_patch); - } diff --git a/docs/dev/debt/libSceFreeTypeOl.md b/docs/dev/debt/libSceFreeTypeOl.md index 9cb669a12b..97d4272c82 100644 --- a/docs/dev/debt/libSceFreeTypeOl.md +++ b/docs/dev/debt/libSceFreeTypeOl.md @@ -2,4 +2,4 @@ ### Known export names; ABI prototypes unverified -- The 150 `sceFreeTypeOlUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOl/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 124 `sceFreeTypeOlUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOl/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From bf92d5cd3d489654e7148d915e4a4b4d0136cf8d Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 07:43:33 +0100 Subject: [PATCH 354/702] test(libSceNetCtlApDialog): cover null argument validation before initialization in GuestNetCtlApDialog --- core/libs/tests/GuestNetCtlApDialog.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestNetCtlApDialog.cpp b/core/libs/tests/GuestNetCtlApDialog.cpp index 82b0318b87..7a3479010e 100644 --- a/core/libs/tests/GuestNetCtlApDialog.cpp +++ b/core/libs/tests/GuestNetCtlApDialog.cpp @@ -37,8 +37,10 @@ int main() { std::memset(result, 0x5a, sizeof(result)); Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); Require(sceNetCtlApDialogOpen(¶m) == kErrNotInitialized); Require(sceNetCtlApDialogClose() == kErrNotInitialized); + Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); Require(sceNetCtlApDialogGetResult(reinterpret_cast<NetCtlApDialogResult*>(result)) == kErrNotInitialized); Require(sceNetCtlApDialogGetStatus() == kStatusNone); Require(sceNetCtlApDialogUpdateStatus() == kStatusNone); From a9d7b30994f2e7d4e76a770847e9f042c2105a04 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 07:46:08 +0100 Subject: [PATCH 355/702] test(libScePlayerReviewDialog): cover mode 1 parameter and null result in initialized state in GuestPlayerReviewDialog --- core/libs/tests/GuestPlayerReviewDialog.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index c14e478429..ccf187396e 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -39,6 +39,7 @@ int main() { Require(scePlayerReviewDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(scePlayerReviewDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(scePlayerReviewDialogGetResult(reinterpret_cast<CommonDialogResultPrefix*>(result)) == COMMON_DIALOG_ERROR_NOT_FINISHED); Require(scePlayerReviewDialogTerminate() == 0); Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); @@ -93,6 +94,14 @@ int main() { Require(result[0] == -1 && result[1] == -1 && result[3] == -1); Require(result[2] == -1); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogInitialize() == 0); + param.mode = 1; + Require(scePlayerReviewDialogOpen(¶m) == 0); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + result[2] = -1; + Require(scePlayerReviewDialogGetResult(reinterpret_cast<CommonDialogResultPrefix*>(result)) == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result[2] == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(scePlayerReviewDialogTerminate() == 0); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceUserServiceTerminate() == 0); } From b9f4f659042f08ee204741ef1a5592aa5632817f Mon Sep 17 00:00:00 2001 From: hugodearaujo <hugoaraujocamara@gmail.com> Date: Tue, 27 Oct 2026 03:46:24 -0300 Subject: [PATCH 356/702] fix(shader): encode both new image fields under disk cache format 49 main took format 48 and a 45th ImageResource field (optionalDescriptor) with #8077; the merge kept both sides's 48 and 44. --- core/shader/recompiler/ShaderDiskCache.cpp | 2 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 882bfb6882..ff1a1518e9 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -141,7 +141,7 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount<ShaderInfo>() == 24, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount<BufferResource>() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount<ImageResource>() == 44, "ImageResource changed: update the info encoder"); +static_assert(fieldCount<ImageResource>() == 45, "ImageResource changed: update the info encoder"); static_assert(fieldCount<SamplerResource>() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount<SampledResourcePair>() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount<StageInput>() == 5, "StageInput changed: update the info encoder"); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 8eba383469..af6bba7ff7 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 48; +inline constexpr std::uint32_t FormatVersion = 49; enum class LoadStatus { Loaded, From 0f9133b7696520316b020b2f48cbf917463ab13d Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 06:46:52 +0000 Subject: [PATCH 357/702] docs(libSceAudioOut): scope the context attribute stub to valid arguments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAudioOut.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 3d22e3c43b..e3e40b2f87 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -5,7 +5,7 @@ - [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 13.60 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are - [sceAudioOutMasteringTerm](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - returns 0 without doing anything: there is no mastering stage to tear down, and `sceAudioOutMasteringInit` keeps no state. Whether the console fails a Term without a matching Init is unknown - [sceAudioOutSetMainOutput](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - returns 0 and does nothing, as the console function does on firmware 12.70: [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L25) reads that export as `xor eax,eax; ret`. The export reads no argument, so its parameters stay unknown; other firmware versions are unverified -- [sceAudioOut2ContextSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - returns 0 and ignores every attribute; what a context attribute changes on a console is unknown. PPSA25286 01.000.568 sets attribute 2 to a 4-byte 1 at start-up, and PPSA02225 passes two attributes from an object at its single call. +- [sceAudioOut2ContextSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - for a valid context and attribute list, returns 0 and ignores every attribute (an invalid context gives `INVALID_HANDLE`, a null list with a nonzero count `INVALID_ARGUMENT`); what a context attribute changes on a console is unknown. PPSA25286 01.000.568 sets attribute 2 to a 4-byte 1 at start-up, and PPSA02225 passes two attributes from an object at its single call. ### Unknown function info From f198f055473ccf85260f68ac0d4a82095c352255 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 06:47:14 +0000 Subject: [PATCH 358/702] docs(libkernel): scope the sceKernelVirtualQuery reads to the inspected instructions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 1d02cecc60..2038bdf2f2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -307,7 +307,7 @@ - [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`, leaving errno unchanged on success. `setpriority` fails the same way; `PRIO_PROCESS` returns 0 without storing, so `getpriority` stays 0, as PS5 firmware 13.60 did in a payload process for sets of -25, -5, 5 and 25. A valid `PRIO_PGRP` or `PRIO_USER` set throws: unverified. What a title process reads, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises, are not reported or unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `RTP_SET` throws for a real-time thread (sce attributes allow only `SCHED_FIFO`/`SCHED_RR`) and for another thread: a payload process on a PS5 (firmware 12.70) answered type 2, prio 0/15/31 on the calling thread with `EINVAL`; no accepted value or set by another thread's id measured. A non-real-time thread (POSIX attributes store `SCHED_OTHER`), unmeasured, reports 3 with 0, and `RTP_SET` accepts only that. `RTP_LOOKUP` reports the policy's type (10 or 2) and the scheduling priority, as measured, not FreeBSD's 0-31; another function fails with `ESRCH` (2 measured), a null `rtprio` with `EFAULT`, an id of no live thread with `ESRCH` (unknown ids measured). `pthread_setconcurrency` stores 0 or more, returns `EINVAL` (not errno) below 0, as libthr does; `pthread_getconcurrency` returns it, a hint. Morrowind (`mshivam019/morrowind-ps5`) imports them -- [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. Title code agrees with this layout: of 34 calls in PPSA01494, PPSA03984, PPSA18194, PPSA21564, PPSA30839, PPSA31162 and PPSA34502, 31 pass an info size of 0x48, and the 27 with the result on the stack read 64-bit values at 0x0, 0x8 and 0x10, 32-bit values at 0x18 and 0x1c and the flag byte at 0x20 (testing 0x2, 0x10 or 0x50, or masking 0x1f); one in PPSA21564 checks the name's first byte at 0x21. +- [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. Title code agrees with this layout: of 34 calls in PPSA01494, PPSA03984, PPSA18194, PPSA21564, PPSA30839, PPSA31162 and PPSA34502, 31 pass an info size of 0x48; in the 30 instructions after the call, the 27 with the result on the stack read 64-bit values at 0x0, 0x8 and 0x10, 32-bit values at 0x18 and 0x1c and the flag byte at 0x20 (testing 0x2, 0x10 or 0x50, or masking 0x1f); one in PPSA21564 checks the name's first byte at 0x21. - [app0.zar files and directories](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - archive directory snapshots refresh on rewind; ordering and seek cookies are host packaging behavior, not console-measured behavior. Disk entries shadow archive names under the archive component comparison. Virtual directory descriptors reject data I/O, mapping, locks, filesystem queries and metadata mutation before touching their null-device backing; ordinary host directories retain native operations. Archived files remain read-only, with duplicate descriptors sharing their cursor. `lstat`, access and other ordinary path operations on archive-only entries remain unsupported. Validation uses synthetic archives on Windows and WSL, without PS5 or title runs. - [sceKernelLoadStartModule](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) loads a system library named by its console path, `/system/common/lib/<name>.sprx` or the sandboxed `/<sandbox word>/common/lib/<name>.sprx` that `sceKernelGetFsSandboxRandomWord` names, from the host `libs/<name>.prx`, as libSceSysmodule loads modules by ID; a host library has no `module_start`, so `res` is 0. Other system directories, such as `/system/priv/lib`, are not mapped. The OpenOrbis piglet sample loads `libScePigletv2VSH.sprx` and `libScePrecompiledShaders.sprx` this way - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. From d386018d187f548f3d474889800d816b7385a5a9 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 06:47:46 +0000 Subject: [PATCH 359/702] docs(libSceSaveData.native): scope the result checks to the instructions after each call Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceSaveData.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 47e66286f3..2a8acee867 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sceSaveDataPrepare, sceSaveDataCommit](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) return success without looking at their arguments; what preparing and committing a mounted save do on a console is unknown. The same six titles call both. `sceSaveDataPrepare` has 11 call sites: PPSA01494, PPSA30839 and PPSA31162 ignore its result, PPSA03984 takes an error path when it is negative, PPSA34502 checks it at one of two sites and PPSA36591 at two of four. `sceSaveDataCommit` has 14: PPSA01494 and PPSA30839 ignore its result, PPSA03984 and PPSA31162 check its sign, PPSA34502 checks it at all five sites and PPSA36591 at two of four. A failure code from either would change the save flow of the titles that check it, while the stubs keep all six on the success path. +- [sceSaveDataPrepare, sceSaveDataCommit](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) return success without looking at their arguments; what preparing and committing a mounted save do on a console is unknown. The same six titles call both; this counts tests of the result in the three instructions after each call. `sceSaveDataPrepare` has 11 call sites: PPSA01494, PPSA30839 and PPSA31162 test none of them there, PPSA03984 takes an error path when it is negative, PPSA34502 checks it at one of two sites and PPSA36591 at two of four. `sceSaveDataCommit` has 14: PPSA01494 and PPSA30839 test none, PPSA03984 and PPSA31162 check its sign, PPSA34502 checks it at all five sites and PPSA36591 at two of four. A failure code from either would change the save flow of the titles that check it, while the stubs keep all six on the success path. ### Unknown function info From 6252b18b81570eac0a80acc33edc930ed676428c Mon Sep 17 00:00:00 2001 From: ebosido444-ai <ebosido444-ai@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:53:50 +0300 Subject: [PATCH 360/702] fix(web): keep the failure report with a failed conversion A failed inspect or conversion hid the progress panel that held the conversion log, cleared the selected archives and showed a single line, so the only evidence of the failure was gone and a multi-GB input had to be picked again. Compose the message with the phase, the current input, the target, the libraries tag and the last 150 log lines, and show it in the error box under "Failure details" so it can be copied into a report. The report is a pure function in failure-report.mjs and is covered by node tests. --- .github/workflows/web-tests.yml | 3 +- demo/web/app.mjs | 700 +++++++++++++------------ demo/web/failure-report.mjs | 14 + demo/web/tests/failure-report.test.mjs | 33 ++ tools/test_web.py | 2 +- 5 files changed, 409 insertions(+), 343 deletions(-) create mode 100644 demo/web/failure-report.mjs create mode 100644 demo/web/tests/failure-report.test.mjs diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 4c42ec63a0..cd39336b6f 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -33,7 +33,8 @@ jobs: node --check demo/web/app.mjs node --check demo/web/archive.mjs node --check demo/web/batch.mjs + node --check demo/web/failure-report.mjs node --check demo/web/output-directory.mjs node --check demo/web/worker.mjs - name: Test browser converter helpers - run: node --test demo/web/tests/app.test.mjs demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs + run: node --test demo/web/tests/app.test.mjs demo/web/tests/batch.test.mjs demo/web/tests/failure-report.test.mjs demo/web/tests/output-directory.test.mjs diff --git a/demo/web/app.mjs b/demo/web/app.mjs index 56e33dabf0..c2316d2eb3 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -1,341 +1,359 @@ -import { abortAndDiscardOutput, OutputDirectoryTransaction } from "./output-directory.mjs"; -import { batchMemoryEstimate } from "./batch.mjs"; - -const $ = id => document.getElementById(id); -const stages = ["Converting executable", "Converting PRX modules", "Scanning syscalls", "Scanning instructions", "Converting instructions", "Analysing imports", "Building output image", "Writing output"]; -let worker; -let selectedInputs = []; -let summaries = []; -let inspectionComplete = false; -let working = false; -let failing = false; -let sink; -let outputDirectory; -let outputTransaction; -let downloadUrl; -let logs = []; -let release; -const canSaveZip = typeof window.showSaveFilePicker === "function" && window.isSecureContext; -const canStream = typeof window.showDirectoryPicker === "function" && window.isSecureContext; -const supported = window.isSecureContext && window.Worker && window.WebAssembly && window.ReadableStream && window.WritableStream && window.crypto?.subtle; -$("save").querySelector('[value="disk"]').disabled = !canStream; -$("save").querySelector('[value="zip"]').disabled = !canSaveZip; -$("save").value = canSaveZip ? "zip" : "memory"; - -function bytes(value) { - if (value < 1024) return `${value} B`; - const unit = value >= 1024 ** 3 ? 3 : value >= 1024 ** 2 ? 2 : 1; - return `${(value / 1024 ** unit).toFixed(1)} ${["B", "KiB", "MiB", "GiB"][unit]}`; -} - -function sum(values) { - let total = 0; - for (const value of values) { - if (!Number.isSafeInteger(value) || value < 0 || total > Number.MAX_SAFE_INTEGER - value) return null; - total += value; - } - return total; -} - -function update() { - const target = document.querySelector('input[name="target"]:checked').value; - const multi = selectedInputs.length > 1; - const inspected = inspectionComplete && summaries.length === selectedInputs.length; - const folderBlocked = $("save").value === "disk" && (target === "windows" || multi); - const folderOption = $("save").querySelector('[value="disk"]'); - folderOption.disabled = !canStream || target === "windows" || multi; - folderOption.textContent = target === "windows" ? "Save to folder · unavailable for Windows (DLL restrictions)" : multi ? "Save to folder · unavailable for batch output" : "Save to folder · uncompressed"; - const libraryBytes = release?.platforms[target]?.expandedBytes; - const selectedBytes = inspected ? sum(summaries.map(summary => target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes)) : null; - const conversionTooLarge = inspected && summaries.some(summary => (target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes) > 256 * 1024 * 1024); - const iconTooLarge = target === "windows" && inspected && summaries.some(summary => summary.windowsIconTooLarge); - const conversionBlocked = conversionTooLarge || iconTooLarge; - let estimate = null; - let estimateOverflow = false; - if (inspected && libraryBytes !== undefined) { - try { estimate = batchMemoryEstimate(summaries, target, libraryBytes); } - catch (error) { - if (!(error instanceof Error) || error.message !== "ZIP contains an unsupported file size") throw error; - estimateOverflow = true; - } - } - const tooLarge = estimateOverflow || (estimate !== null && estimate > 256 * 1024 * 1024); - $("save").querySelector('[value="memory"]').disabled = Boolean(tooLarge); - const memoryBlocked = $("save").value === "memory" && tooLarge; - if (inspected) { - const totalFiles = sum(summaries.map(summary => summary.files)); - const totalBytes = sum(summaries.map(summary => summary.totalBytes)); - const modules = sum(summaries.map(summary => summary.modules)); - const sizeText = value => value === null ? "beyond the supported size range" : bytes(value); - const countText = value => value === null ? "beyond the supported range" : value; - $("archive-info").textContent = multi - ? `${selectedInputs.length} apps · ${countText(totalFiles)} files · ${sizeText(totalBytes)} expanded · ${countText(modules)} modules · ${sizeText(selectedBytes)} ${target} conversion input` - : `${summaries[0].files} files · ${bytes(summaries[0].totalBytes)} expanded · ${summaries[0].modules} modules · ${bytes(target === "windows" ? summaries[0].windowsSelectedBytes : summaries[0].selectedBytes)} ${target} conversion input` + (summaries[0].root ? ` · Root: ${summaries[0].root}` : ""); - } - $("save-note").textContent = multi ? $("save").value === "memory" ? "Creates one ZIP with each app in its own folder. The complete bundle must fit in the 256 MiB memory-download budget." : "Writes one compressed ZIP containing each app in its own folder, without holding the whole bundle in memory." : $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Incomplete output folders are removed after cancellation or failure. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; - $("browser-note").hidden = supported && canStream && canSaveZip && !memoryBlocked && !folderBlocked && !conversionBlocked; - $("browser-note").textContent = !supported ? "This browser or connection does not support the converter. Use a current browser over HTTPS, or use the native relinker." : folderBlocked && multi ? "Batch conversion creates one ZIP bundle. Choose Save ZIP to disk or Download in memory." : folderBlocked ? "Browsers block direct DLL creation. Windows output requires ZIP. Choose Save ZIP to disk or Download in memory." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : estimateOverflow ? "The combined output exceeds the browser's supported size range. Choose Save ZIP to disk." : memoryBlocked ? "This output exceeds the memory-download limit. Choose Save ZIP to disk. If that option is unavailable, open this page in desktop Chrome or Edge." : "Some direct disk saving options are unavailable in this browser or context. Small apps can still be downloaded in memory. For larger apps, use desktop Chrome or Edge over HTTPS."; - const hasWarnings = inspected && summaries.some(summary => summary.warnings.length > 0); - $("convert").disabled = !supported || working || !inspected || !release || conversionBlocked || memoryBlocked || folderBlocked || (hasWarnings && !$("acknowledge").checked); - $("ready-note").textContent = inspected ? (folderBlocked && multi ? "Choose a ZIP save mode for the app bundle." : folderBlocked ? "Choose a ZIP save mode for Windows." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : memoryBlocked ? "A direct disk save is required for this output." : release ? `Libraries: ${release.tag} · ${multi ? "ZIP bundle" : $("save").value === "disk" ? "Uncompressed folder" : "ZIP compression level 9"}` : "Loading release information…") : "Choose one folder or one or more ZIP files to get started."; -} - -function lock(value) { - for (const id of ["choose", "choose-folder", "change", "target", "save", "intel"]) $(id).disabled = value || !supported || (id === "choose-folder" && !("webkitdirectory" in $("folder"))); -} - -async function fail(message) { - if (failing) return; - failing = true; - worker?.terminate(); - worker = null; - working = true; - selectedInputs = []; - summaries = []; - inspectionComplete = false; - const transaction = outputTransaction; - outputTransaction = null; - outputDirectory = null; - if (transaction) { - $("status").hidden = false; - $("phase").textContent = "Removing incomplete output folder"; - } - const activeSink = sink; - sink = null; - for (const failure of await abortAndDiscardOutput(transaction, activeSink)) message += `\n${failure}`; - $("error").textContent = message; - $("error").hidden = false; - $("status").hidden = true; - $("cancel").hidden = true; - $("drop").hidden = false; - $("selection").hidden = true; - $("warnings").hidden = true; - $("app-list").replaceChildren(); - $("file").value = ""; - working = false; - failing = false; - lock(false); - update(); -} - -async function onMessage({ data }) { - try { - if (data.type === "progress") { - if (data.message) { - logs.push(`${data.appName ? `${data.appName}: ` : ""}${data.message.trim()}`); - logs = logs.slice(-150); - $("log").textContent = logs.join("\n"); - return; - } - const phase = data.phase ?? stages[data.stage]; - $("phase").textContent = data.appCount > 1 ? `App ${data.appIndex + 1}/${data.appCount} · ${phase}` : phase; - $("current-file").textContent = [data.appName, data.input].filter(Boolean).join(" · "); - if (Number.isFinite(data.total) && data.total > 0) { - const percent = Math.min(100, Math.floor(data.completed / data.total * 100)); - $("progress").value = percent; - $("percent").textContent = `${percent}%`; - } else { - $("progress").removeAttribute("value"); - $("percent").textContent = ""; - } - } else if (data.type === "inspected") { - summaries[data.index] = data.summary; - } else if (data.type === "inspection-complete") { - inspectionComplete = true; - working = false; - $("selection").hidden = false; - $("drop").hidden = true; - $("archive-name").textContent = selectedInputs.length > 1 ? `${selectedInputs.length} app ZIPs` : selectedInputs[0].name; - $("app-list").replaceChildren(...selectedInputs.map((input, index) => { - const item = document.createElement("li"); - const name = document.createElement("strong"); - name.textContent = input.name; - const details = document.createElement("span"); - const summary = summaries[index]; - details.textContent = `${summary.files} files · ${bytes(summary.totalBytes)} expanded · ${summary.modules} modules`; - item.append(name, details); - return item; - })); - $("app-list").hidden = selectedInputs.length < 2; - const warningItems = []; - for (let index = 0; index < summaries.length; index++) { - for (const message of summaries[index].warnings) { - const item = document.createElement("li"); - item.textContent = selectedInputs.length > 1 ? `${selectedInputs[index].name}: ${message}` : message; - warningItems.push(item); - } - } - $("warning-list").replaceChildren(...warningItems); - $("warnings").hidden = warningItems.length === 0; - $("status").hidden = true; - $("cancel").hidden = true; - lock(false); - update(); - } else if (data.type === "directory" || data.type === "open-file") { - if (!outputDirectory) throw new Error("No output folder is open"); - const parts = data.name.replace(/\/$/u, "").split("/"); - if (parts.some(part => !part || part === "." || part === ".." || part.includes("\\"))) throw new Error("Invalid output path"); - const filename = data.type === "open-file" ? parts.pop() : null; - let parent = outputDirectory; - for (const part of parts) parent = await parent.getDirectoryHandle(part, { create: true }); - if (filename) { - try { - const handle = await parent.getFileHandle(filename, { create: true }); - sink = await handle.createWritable(); - } catch (error) { - throw new Error(`Cannot write ${data.name}: ${error.message}. Choose ZIP output if the browser blocks this file type.`); - } - } - worker?.postMessage({ type: "written" }); - } else if (data.type === "close-file") { - if (!sink) throw new Error("No output file is open"); - await sink.close(); - sink = null; - worker?.postMessage({ type: "written" }); - } else if (data.type === "write") { - if (!sink) throw new Error("No output file is open"); - await sink.write(data.bytes); - worker?.postMessage({ type: "written" }); - } else if (data.type === "complete") { - if (sink) { await sink.close(); sink = null; } - working = false; - $("done").hidden = false; - $("status").hidden = true; - $("cancel").hidden = true; - $("result-info").textContent = `${data.apps > 1 ? `${data.apps} apps · ` : ""}${bytes(data.size)} ${data.mode === "disk" ? "files" : "ZIP"} · ${data.release} system libraries · ${data.apps > 1 ? "combined app bundle" : data.outputName}`; - if (data.blob) { - downloadUrl = URL.createObjectURL(data.blob); - $("download").href = downloadUrl; - $("download").download = `${data.outputName}.zip`; - $("download").hidden = false; - } else if (data.mode === "zip") { - $("result-info").textContent += " · ZIP saved to disk"; - } else { - $("result-info").textContent += ` · Saved in ${outputTransaction.name}/`; - if (document.querySelector('input[name="target"]:checked').value === "linux") $("result-info").textContent += ` · On Linux, run chmod +x "${data.outputName}" before launching.`; - } - $("result-note").textContent = data.apps > 1 ? "Extract the ZIP; each app is in its own folder with its executable, libs and app0 files. On Linux, run chmod +x */*.elf before launching." : "Extract the entire ZIP before launching the executable."; - $("result-note").hidden = data.mode === "disk"; - outputDirectory = null; - outputTransaction?.commit(); - outputTransaction = null; - selectedInputs = []; - summaries = []; - inspectionComplete = false; - worker.terminate(); - worker = null; - lock(false); - update(); - } else if (data.type === "error") await fail(data.message); - } catch (error) { await fail(error.message); } -} - -async function selectInputs(inputs) { - if (working || !supported || !inputs.length) return; - if (inputs.some(input => !input.files && !input.name.toLowerCase().endsWith(".zip")) || (inputs.length > 1 && inputs.some(input => input.files))) { - await fail("Batch conversion accepts ZIP archives only. Choose one folder or select ZIP files."); - return; - } - worker?.terminate(); - if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } - selectedInputs = inputs; - summaries = []; - inspectionComplete = false; - working = true; - logs = []; - $("log").textContent = ""; - for (const id of ["error", "done", "download", "warnings", "selection"]) $(id).hidden = true; - $("acknowledge").checked = false; - $("status").hidden = false; - $("cancel").hidden = false; - $("phase").textContent = "Inspecting files"; - $("current-file").textContent = inputs.length > 1 ? `Inspecting ${inputs.length} ZIP archives` : inputs[0].name; - $("percent").textContent = ""; - $("progress").removeAttribute("value"); - lock(true); - update(); - worker = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" }); - worker.onmessage = onMessage; - worker.onerror = event => fail(event.message || "The converter worker stopped. Try a smaller app or use the native relinker."); - worker.postMessage({ type: "inspect", inputs }); -} - -$("choose").onclick = () => { $("file").value = ""; $("file").click(); }; -$("change").onclick = () => { - if (working) return; - worker?.terminate(); - worker = null; - if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } - selectedInputs = []; - summaries = []; - inspectionComplete = false; - $("file").value = ""; - $("folder").value = ""; - $("app-list").replaceChildren(); - for (const id of ["selection", "warnings", "error", "done", "download", "app-list"]) $(id).hidden = true; - $("acknowledge").checked = false; - $("drop").hidden = false; - update(); -}; -$("choose-folder").onclick = () => { $("folder").value = ""; $("folder").click(); }; -$("folder").onchange = () => { - const files = [...$("folder").files].map(file => ({ path: file.webkitRelativePath, file })); - if (files.length) selectInputs([{ name: files[0].path.split("/")[0], files }]); -}; -$("file").onchange = () => { - const inputs = [...$("file").files].map(file => ({ name: file.name, file })); - $("file").value = ""; - selectInputs(inputs); -}; -for (const type of ["dragenter", "dragover"]) $("drop").addEventListener(type, event => { event.preventDefault(); if (!working) $("drop").classList.add("dragging"); }); -$("drop").ondragleave = () => $("drop").classList.remove("dragging"); -$("drop").ondrop = event => { - event.preventDefault(); - $("drop").classList.remove("dragging"); - if (working) return; - selectInputs([...event.dataTransfer.files].map(file => ({ name: file.name, file }))); -}; -for (const id of ["save", "target", "acknowledge"]) $(id).addEventListener("change", update); -$("cancel").onclick = () => fail("Cancelled. Choose the ZIP or folder again to restart."); -$("convert").onclick = async () => { - if ($("convert").disabled) return; - working = true; - lock(true); - update(); - try { - const target = document.querySelector('input[name="target"]:checked').value; - if ($("save").value === "zip") { - const name = selectedInputs.length > 1 ? `anyps5-${target}-apps.zip` : `${selectedInputs[0].name.replace(/\.zip$/iu, "")}-${target}.zip`; - const handle = await window.showSaveFilePicker({ suggestedName: name, types: [{ description: "ZIP archive", accept: { "application/zip": [".zip"] } }] }); - sink = await handle.createWritable(); - } else if ($("save").value === "disk") { - const parent = await window.showDirectoryPicker({ mode: "readwrite" }); - const name = selectedInputs[0].name.replace(/\.zip$/iu, "") + "-" + target; - outputTransaction = await OutputDirectoryTransaction.create(parent, name); - outputDirectory = outputTransaction.directory; - } - $("status").hidden = false; - $("cancel").hidden = false; - $("error").hidden = true; - worker.postMessage({ type: "convert", target, toIntel: $("intel").checked, mode: $("save").value }); - } catch (error) { - if (error.name === "AbortError") { working = false; lock(false); update(); } - else await fail(error.message); - } -}; -$("theme").onclick = () => { - const dark = document.documentElement.dataset.theme ? document.documentElement.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches; - document.documentElement.dataset.theme = dark ? "light" : "dark"; -}; -update(); -lock(false); -if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status}).${response.status === 404 ? " release.json is generated during the site build, so it is missing when serving demo/web directly." : ""}`); return response.json(); }).then(value => { - if (!value.tag || !["windows", "linux"].every(target => Number.isSafeInteger(value.platforms?.[target]?.expandedBytes))) throw new Error("Invalid release information"); - release = value; - update(); -}).catch(error => fail(error.message)); - -if (!("webkitdirectory" in $("folder"))) { - $("choose-folder").disabled = true; - $("choose-folder").title = "Folder selection is unavailable in this browser. Choose a ZIP instead."; -} +import { abortAndDiscardOutput, OutputDirectoryTransaction } from "./output-directory.mjs"; +import { batchMemoryEstimate } from "./batch.mjs"; +import { failureReport } from "./failure-report.mjs"; + +const $ = id => document.getElementById(id); +const stages = ["Converting executable", "Converting PRX modules", "Scanning syscalls", "Scanning instructions", "Converting instructions", "Analysing imports", "Building output image", "Writing output"]; +let worker; +let selectedInputs = []; +let summaries = []; +let inspectionComplete = false; +let working = false; +let failing = false; +let sink; +let outputDirectory; +let outputTransaction; +let downloadUrl; +let logs = []; +let release; +const canSaveZip = typeof window.showSaveFilePicker === "function" && window.isSecureContext; +const canStream = typeof window.showDirectoryPicker === "function" && window.isSecureContext; +const supported = window.isSecureContext && window.Worker && window.WebAssembly && window.ReadableStream && window.WritableStream && window.crypto?.subtle; +$("save").querySelector('[value="disk"]').disabled = !canStream; +$("save").querySelector('[value="zip"]').disabled = !canSaveZip; +$("save").value = canSaveZip ? "zip" : "memory"; + +function bytes(value) { + if (value < 1024) return `${value} B`; + const unit = value >= 1024 ** 3 ? 3 : value >= 1024 ** 2 ? 2 : 1; + return `${(value / 1024 ** unit).toFixed(1)} ${["B", "KiB", "MiB", "GiB"][unit]}`; +} + +function sum(values) { + let total = 0; + for (const value of values) { + if (!Number.isSafeInteger(value) || value < 0 || total > Number.MAX_SAFE_INTEGER - value) return null; + total += value; + } + return total; +} + +function update() { + const target = document.querySelector('input[name="target"]:checked').value; + const multi = selectedInputs.length > 1; + const inspected = inspectionComplete && summaries.length === selectedInputs.length; + const folderBlocked = $("save").value === "disk" && (target === "windows" || multi); + const folderOption = $("save").querySelector('[value="disk"]'); + folderOption.disabled = !canStream || target === "windows" || multi; + folderOption.textContent = target === "windows" ? "Save to folder · unavailable for Windows (DLL restrictions)" : multi ? "Save to folder · unavailable for batch output" : "Save to folder · uncompressed"; + const libraryBytes = release?.platforms[target]?.expandedBytes; + const selectedBytes = inspected ? sum(summaries.map(summary => target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes)) : null; + const conversionTooLarge = inspected && summaries.some(summary => (target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes) > 256 * 1024 * 1024); + const iconTooLarge = target === "windows" && inspected && summaries.some(summary => summary.windowsIconTooLarge); + const conversionBlocked = conversionTooLarge || iconTooLarge; + let estimate = null; + let estimateOverflow = false; + if (inspected && libraryBytes !== undefined) { + try { estimate = batchMemoryEstimate(summaries, target, libraryBytes); } + catch (error) { + if (!(error instanceof Error) || error.message !== "ZIP contains an unsupported file size") throw error; + estimateOverflow = true; + } + } + const tooLarge = estimateOverflow || (estimate !== null && estimate > 256 * 1024 * 1024); + $("save").querySelector('[value="memory"]').disabled = Boolean(tooLarge); + const memoryBlocked = $("save").value === "memory" && tooLarge; + if (inspected) { + const totalFiles = sum(summaries.map(summary => summary.files)); + const totalBytes = sum(summaries.map(summary => summary.totalBytes)); + const modules = sum(summaries.map(summary => summary.modules)); + const sizeText = value => value === null ? "beyond the supported size range" : bytes(value); + const countText = value => value === null ? "beyond the supported range" : value; + $("archive-info").textContent = multi + ? `${selectedInputs.length} apps · ${countText(totalFiles)} files · ${sizeText(totalBytes)} expanded · ${countText(modules)} modules · ${sizeText(selectedBytes)} ${target} conversion input` + : `${summaries[0].files} files · ${bytes(summaries[0].totalBytes)} expanded · ${summaries[0].modules} modules · ${bytes(target === "windows" ? summaries[0].windowsSelectedBytes : summaries[0].selectedBytes)} ${target} conversion input` + (summaries[0].root ? ` · Root: ${summaries[0].root}` : ""); + } + $("save-note").textContent = multi ? $("save").value === "memory" ? "Creates one ZIP with each app in its own folder. The complete bundle must fit in the 256 MiB memory-download budget." : "Writes one compressed ZIP containing each app in its own folder, without holding the whole bundle in memory." : $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Incomplete output folders are removed after cancellation or failure. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; + $("browser-note").hidden = supported && canStream && canSaveZip && !memoryBlocked && !folderBlocked && !conversionBlocked; + $("browser-note").textContent = !supported ? "This browser or connection does not support the converter. Use a current browser over HTTPS, or use the native relinker." : folderBlocked && multi ? "Batch conversion creates one ZIP bundle. Choose Save ZIP to disk or Download in memory." : folderBlocked ? "Browsers block direct DLL creation. Windows output requires ZIP. Choose Save ZIP to disk or Download in memory." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : estimateOverflow ? "The combined output exceeds the browser's supported size range. Choose Save ZIP to disk." : memoryBlocked ? "This output exceeds the memory-download limit. Choose Save ZIP to disk. If that option is unavailable, open this page in desktop Chrome or Edge." : "Some direct disk saving options are unavailable in this browser or context. Small apps can still be downloaded in memory. For larger apps, use desktop Chrome or Edge over HTTPS."; + const hasWarnings = inspected && summaries.some(summary => summary.warnings.length > 0); + $("convert").disabled = !supported || working || !inspected || !release || conversionBlocked || memoryBlocked || folderBlocked || (hasWarnings && !$("acknowledge").checked); + $("ready-note").textContent = inspected ? (folderBlocked && multi ? "Choose a ZIP save mode for the app bundle." : folderBlocked ? "Choose a ZIP save mode for Windows." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : memoryBlocked ? "A direct disk save is required for this output." : release ? `Libraries: ${release.tag} · ${multi ? "ZIP bundle" : $("save").value === "disk" ? "Uncompressed folder" : "ZIP compression level 9"}` : "Loading release information…") : "Choose one folder or one or more ZIP files to get started."; +} + +function lock(value) { + for (const id of ["choose", "choose-folder", "change", "target", "save", "intel"]) $(id).disabled = value || !supported || (id === "choose-folder" && !("webkitdirectory" in $("folder"))); +} + +async function fail(message) { + if (failing) return; + failing = true; + const report = failureReport({ + message, + phase: $("phase").textContent, + input: $("current-file").textContent, + target: document.querySelector('input[name="target"]:checked').value, + release, + log: logs + }); + worker?.terminate(); + worker = null; + working = true; + selectedInputs = []; + summaries = []; + inspectionComplete = false; + const transaction = outputTransaction; + outputTransaction = null; + outputDirectory = null; + if (transaction) { + $("status").hidden = false; + $("phase").textContent = "Removing incomplete output folder"; + } + const activeSink = sink; + sink = null; + for (const failure of await abortAndDiscardOutput(transaction, activeSink)) message += `\n${failure}`; + if (report === message) $("error").textContent = message; + else { + const details = document.createElement("details"); + const summary = document.createElement("summary"); + summary.textContent = "Failure details"; + const text = document.createElement("pre"); + text.textContent = report; + details.append(summary, text); + $("error").replaceChildren(document.createTextNode(message), details); + } + $("error").hidden = false; + $("status").hidden = true; + $("cancel").hidden = true; + $("drop").hidden = false; + $("selection").hidden = true; + $("warnings").hidden = true; + $("app-list").replaceChildren(); + $("file").value = ""; + working = false; + failing = false; + lock(false); + update(); +} + +async function onMessage({ data }) { + try { + if (data.type === "progress") { + if (data.message) { + logs.push(`${data.appName ? `${data.appName}: ` : ""}${data.message.trim()}`); + logs = logs.slice(-150); + $("log").textContent = logs.join("\n"); + return; + } + const phase = data.phase ?? stages[data.stage]; + $("phase").textContent = data.appCount > 1 ? `App ${data.appIndex + 1}/${data.appCount} · ${phase}` : phase; + $("current-file").textContent = [data.appName, data.input].filter(Boolean).join(" · "); + if (Number.isFinite(data.total) && data.total > 0) { + const percent = Math.min(100, Math.floor(data.completed / data.total * 100)); + $("progress").value = percent; + $("percent").textContent = `${percent}%`; + } else { + $("progress").removeAttribute("value"); + $("percent").textContent = ""; + } + } else if (data.type === "inspected") { + summaries[data.index] = data.summary; + } else if (data.type === "inspection-complete") { + inspectionComplete = true; + working = false; + $("selection").hidden = false; + $("drop").hidden = true; + $("archive-name").textContent = selectedInputs.length > 1 ? `${selectedInputs.length} app ZIPs` : selectedInputs[0].name; + $("app-list").replaceChildren(...selectedInputs.map((input, index) => { + const item = document.createElement("li"); + const name = document.createElement("strong"); + name.textContent = input.name; + const details = document.createElement("span"); + const summary = summaries[index]; + details.textContent = `${summary.files} files · ${bytes(summary.totalBytes)} expanded · ${summary.modules} modules`; + item.append(name, details); + return item; + })); + $("app-list").hidden = selectedInputs.length < 2; + const warningItems = []; + for (let index = 0; index < summaries.length; index++) { + for (const message of summaries[index].warnings) { + const item = document.createElement("li"); + item.textContent = selectedInputs.length > 1 ? `${selectedInputs[index].name}: ${message}` : message; + warningItems.push(item); + } + } + $("warning-list").replaceChildren(...warningItems); + $("warnings").hidden = warningItems.length === 0; + $("status").hidden = true; + $("cancel").hidden = true; + lock(false); + update(); + } else if (data.type === "directory" || data.type === "open-file") { + if (!outputDirectory) throw new Error("No output folder is open"); + const parts = data.name.replace(/\/$/u, "").split("/"); + if (parts.some(part => !part || part === "." || part === ".." || part.includes("\\"))) throw new Error("Invalid output path"); + const filename = data.type === "open-file" ? parts.pop() : null; + let parent = outputDirectory; + for (const part of parts) parent = await parent.getDirectoryHandle(part, { create: true }); + if (filename) { + try { + const handle = await parent.getFileHandle(filename, { create: true }); + sink = await handle.createWritable(); + } catch (error) { + throw new Error(`Cannot write ${data.name}: ${error.message}. Choose ZIP output if the browser blocks this file type.`); + } + } + worker?.postMessage({ type: "written" }); + } else if (data.type === "close-file") { + if (!sink) throw new Error("No output file is open"); + await sink.close(); + sink = null; + worker?.postMessage({ type: "written" }); + } else if (data.type === "write") { + if (!sink) throw new Error("No output file is open"); + await sink.write(data.bytes); + worker?.postMessage({ type: "written" }); + } else if (data.type === "complete") { + if (sink) { await sink.close(); sink = null; } + working = false; + $("done").hidden = false; + $("status").hidden = true; + $("cancel").hidden = true; + $("result-info").textContent = `${data.apps > 1 ? `${data.apps} apps · ` : ""}${bytes(data.size)} ${data.mode === "disk" ? "files" : "ZIP"} · ${data.release} system libraries · ${data.apps > 1 ? "combined app bundle" : data.outputName}`; + if (data.blob) { + downloadUrl = URL.createObjectURL(data.blob); + $("download").href = downloadUrl; + $("download").download = `${data.outputName}.zip`; + $("download").hidden = false; + } else if (data.mode === "zip") { + $("result-info").textContent += " · ZIP saved to disk"; + } else { + $("result-info").textContent += ` · Saved in ${outputTransaction.name}/`; + if (document.querySelector('input[name="target"]:checked').value === "linux") $("result-info").textContent += ` · On Linux, run chmod +x "${data.outputName}" before launching.`; + } + $("result-note").textContent = data.apps > 1 ? "Extract the ZIP; each app is in its own folder with its executable, libs and app0 files. On Linux, run chmod +x */*.elf before launching." : "Extract the entire ZIP before launching the executable."; + $("result-note").hidden = data.mode === "disk"; + outputDirectory = null; + outputTransaction?.commit(); + outputTransaction = null; + selectedInputs = []; + summaries = []; + inspectionComplete = false; + worker.terminate(); + worker = null; + lock(false); + update(); + } else if (data.type === "error") await fail(data.message); + } catch (error) { await fail(error.message); } +} + +async function selectInputs(inputs) { + if (working || !supported || !inputs.length) return; + if (inputs.some(input => !input.files && !input.name.toLowerCase().endsWith(".zip")) || (inputs.length > 1 && inputs.some(input => input.files))) { + await fail("Batch conversion accepts ZIP archives only. Choose one folder or select ZIP files."); + return; + } + worker?.terminate(); + if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } + selectedInputs = inputs; + summaries = []; + inspectionComplete = false; + working = true; + logs = []; + $("log").textContent = ""; + for (const id of ["error", "done", "download", "warnings", "selection"]) $(id).hidden = true; + $("acknowledge").checked = false; + $("status").hidden = false; + $("cancel").hidden = false; + $("phase").textContent = "Inspecting files"; + $("current-file").textContent = inputs.length > 1 ? `Inspecting ${inputs.length} ZIP archives` : inputs[0].name; + $("percent").textContent = ""; + $("progress").removeAttribute("value"); + lock(true); + update(); + worker = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" }); + worker.onmessage = onMessage; + worker.onerror = event => fail(event.message || "The converter worker stopped. Try a smaller app or use the native relinker."); + worker.postMessage({ type: "inspect", inputs }); +} + +$("choose").onclick = () => { $("file").value = ""; $("file").click(); }; +$("change").onclick = () => { + if (working) return; + worker?.terminate(); + worker = null; + if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } + selectedInputs = []; + summaries = []; + inspectionComplete = false; + $("file").value = ""; + $("folder").value = ""; + $("app-list").replaceChildren(); + for (const id of ["selection", "warnings", "error", "done", "download", "app-list"]) $(id).hidden = true; + $("acknowledge").checked = false; + $("drop").hidden = false; + update(); +}; +$("choose-folder").onclick = () => { $("folder").value = ""; $("folder").click(); }; +$("folder").onchange = () => { + const files = [...$("folder").files].map(file => ({ path: file.webkitRelativePath, file })); + if (files.length) selectInputs([{ name: files[0].path.split("/")[0], files }]); +}; +$("file").onchange = () => { + const inputs = [...$("file").files].map(file => ({ name: file.name, file })); + $("file").value = ""; + selectInputs(inputs); +}; +for (const type of ["dragenter", "dragover"]) $("drop").addEventListener(type, event => { event.preventDefault(); if (!working) $("drop").classList.add("dragging"); }); +$("drop").ondragleave = () => $("drop").classList.remove("dragging"); +$("drop").ondrop = event => { + event.preventDefault(); + $("drop").classList.remove("dragging"); + if (working) return; + selectInputs([...event.dataTransfer.files].map(file => ({ name: file.name, file }))); +}; +for (const id of ["save", "target", "acknowledge"]) $(id).addEventListener("change", update); +$("cancel").onclick = () => fail("Cancelled. Choose the ZIP or folder again to restart."); +$("convert").onclick = async () => { + if ($("convert").disabled) return; + working = true; + lock(true); + update(); + try { + const target = document.querySelector('input[name="target"]:checked').value; + if ($("save").value === "zip") { + const name = selectedInputs.length > 1 ? `anyps5-${target}-apps.zip` : `${selectedInputs[0].name.replace(/\.zip$/iu, "")}-${target}.zip`; + const handle = await window.showSaveFilePicker({ suggestedName: name, types: [{ description: "ZIP archive", accept: { "application/zip": [".zip"] } }] }); + sink = await handle.createWritable(); + } else if ($("save").value === "disk") { + const parent = await window.showDirectoryPicker({ mode: "readwrite" }); + const name = selectedInputs[0].name.replace(/\.zip$/iu, "") + "-" + target; + outputTransaction = await OutputDirectoryTransaction.create(parent, name); + outputDirectory = outputTransaction.directory; + } + $("status").hidden = false; + $("cancel").hidden = false; + $("error").hidden = true; + worker.postMessage({ type: "convert", target, toIntel: $("intel").checked, mode: $("save").value }); + } catch (error) { + if (error.name === "AbortError") { working = false; lock(false); update(); } + else await fail(error.message); + } +}; +$("theme").onclick = () => { + const dark = document.documentElement.dataset.theme ? document.documentElement.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches; + document.documentElement.dataset.theme = dark ? "light" : "dark"; +}; +update(); +lock(false); +if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status}).${response.status === 404 ? " release.json is generated during the site build, so it is missing when serving demo/web directly." : ""}`); return response.json(); }).then(value => { + if (!value.tag || !["windows", "linux"].every(target => Number.isSafeInteger(value.platforms?.[target]?.expandedBytes))) throw new Error("Invalid release information"); + release = value; + update(); +}).catch(error => fail(error.message)); + +if (!("webkitdirectory" in $("folder"))) { + $("choose-folder").disabled = true; + $("choose-folder").title = "Folder selection is unavailable in this browser. Choose a ZIP instead."; +} diff --git a/demo/web/failure-report.mjs b/demo/web/failure-report.mjs new file mode 100644 index 0000000000..3bf6144acf --- /dev/null +++ b/demo/web/failure-report.mjs @@ -0,0 +1,14 @@ +const LOG_TAIL = 150; + +export function failureReport({ message, phase, input, target, release, log }) { + const context = []; + if (phase) context.push(`Phase: ${phase}`); + if (input) context.push(`Input: ${input}`); + if (target) context.push(`Target: ${target}`); + if (release?.tag) context.push(`Libraries: ${release.tag}`); + const tail = (log ?? []).slice(-LOG_TAIL); + const sections = [String(message)]; + if (context.length) sections.push(context.join("\n")); + if (tail.length) sections.push(`Conversion log (last ${tail.length} lines):\n${tail.join("\n")}`); + return sections.join("\n\n"); +} diff --git a/demo/web/tests/failure-report.test.mjs b/demo/web/tests/failure-report.test.mjs new file mode 100644 index 0000000000..04b815d9b0 --- /dev/null +++ b/demo/web/tests/failure-report.test.mjs @@ -0,0 +1,33 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { failureReport } from "../failure-report.mjs"; + +test("names the input, the phase and the libraries with the failure", () => { + const report = failureReport({ + message: "FAIL: Executable output would overwrite the input executable", + phase: "Converting PRX modules", + input: "PPSA00000.zip \u00b7 app0/sce_module/libScePad.prx", + target: "windows", + release: { tag: "v1.4.4" }, + log: ["Inspecting archive", "FAIL: Executable output would overwrite the input executable"] + }); + assert.match(report, /^FAIL: Executable output would overwrite the input executable/u); + assert.match(report, /Phase: Converting PRX modules/u); + assert.match(report, /Target: windows/u); + assert.match(report, /Libraries: v1\.4\.4/u); + assert.match(report, /Conversion log \(last 2 lines\):\nInspecting archive\nFAIL:/u); +}); + +test("keeps only the last 150 log lines", () => { + const log = Array.from({ length: 200 }, (_, index) => `line ${index + 1}`); + const report = failureReport({ message: "cancelled", log }); + const lines = report.split("\n").filter(line => line.startsWith("line ")); + assert.equal(lines.length, 150); + assert.equal(lines[0], "line 51"); + assert.equal(lines.at(-1), "line 200"); +}); + +test("prints a bare message when there is no context or log", () => { + assert.equal(failureReport({ message: "cancelled" }), "cancelled"); + assert.equal(failureReport({ message: "cancelled", log: [] }), "cancelled"); +}); diff --git a/tools/test_web.py b/tools/test_web.py index 03fe29a0c7..c3dc394494 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -27,7 +27,7 @@ def main(argv=None): parser.error(f"archive module does not exist: {archive}") try: subprocess.run(["node", "--test", *(str(ROOT / "demo/web/tests" / name) for name in ( - "app.test.mjs", "batch.test.mjs", "output-directory.test.mjs" + "app.test.mjs", "batch.test.mjs", "failure-report.test.mjs", "output-directory.test.mjs" ))], check=True) with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: fixture = Path(directory) From 638d81581c127f8346efc0e2ded36a51674bd30d Mon Sep 17 00:00:00 2001 From: ebosido444-ai <ebosido444-ai@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:59:46 +0300 Subject: [PATCH 361/702] test(web): ship failure-report.mjs with the web release The module is imported by app.mjs, so the release archive and its fixture have to carry it; the packaging test listed it as missing. --- tools/package_release.py | 2 +- tools/tests/test_web_release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/package_release.py b/tools/package_release.py index ec4549d801..2cafffac3b 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -11,7 +11,7 @@ import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent -WEB_FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "output-directory.mjs", "worker.mjs", +WEB_FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "failure-report.mjs", "output-directory.mjs", "worker.mjs", "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index f33e48e6a5..8e4e38c145 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -16,7 +16,7 @@ import build_web import package_release import test_web -FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "output-directory.mjs", "worker.mjs", +FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "failure-report.mjs", "output-directory.mjs", "worker.mjs", "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") REVISION = "a" * 40 LATEST = "https://api.github.com/repos/boykopovar/AnyPS5/releases/latest" From e327745137ddb0d55c664e354faadc6e40359e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Tue, 27 Oct 2026 07:52:39 +0100 Subject: [PATCH 362/702] fix(web): restore LF line endings in app.mjs --- demo/web/app.mjs | 718 +++++++++++++++++++++++------------------------ 1 file changed, 359 insertions(+), 359 deletions(-) diff --git a/demo/web/app.mjs b/demo/web/app.mjs index c2316d2eb3..6a17bb6ddf 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -1,359 +1,359 @@ -import { abortAndDiscardOutput, OutputDirectoryTransaction } from "./output-directory.mjs"; -import { batchMemoryEstimate } from "./batch.mjs"; -import { failureReport } from "./failure-report.mjs"; - -const $ = id => document.getElementById(id); -const stages = ["Converting executable", "Converting PRX modules", "Scanning syscalls", "Scanning instructions", "Converting instructions", "Analysing imports", "Building output image", "Writing output"]; -let worker; -let selectedInputs = []; -let summaries = []; -let inspectionComplete = false; -let working = false; -let failing = false; -let sink; -let outputDirectory; -let outputTransaction; -let downloadUrl; -let logs = []; -let release; -const canSaveZip = typeof window.showSaveFilePicker === "function" && window.isSecureContext; -const canStream = typeof window.showDirectoryPicker === "function" && window.isSecureContext; -const supported = window.isSecureContext && window.Worker && window.WebAssembly && window.ReadableStream && window.WritableStream && window.crypto?.subtle; -$("save").querySelector('[value="disk"]').disabled = !canStream; -$("save").querySelector('[value="zip"]').disabled = !canSaveZip; -$("save").value = canSaveZip ? "zip" : "memory"; - -function bytes(value) { - if (value < 1024) return `${value} B`; - const unit = value >= 1024 ** 3 ? 3 : value >= 1024 ** 2 ? 2 : 1; - return `${(value / 1024 ** unit).toFixed(1)} ${["B", "KiB", "MiB", "GiB"][unit]}`; -} - -function sum(values) { - let total = 0; - for (const value of values) { - if (!Number.isSafeInteger(value) || value < 0 || total > Number.MAX_SAFE_INTEGER - value) return null; - total += value; - } - return total; -} - -function update() { - const target = document.querySelector('input[name="target"]:checked').value; - const multi = selectedInputs.length > 1; - const inspected = inspectionComplete && summaries.length === selectedInputs.length; - const folderBlocked = $("save").value === "disk" && (target === "windows" || multi); - const folderOption = $("save").querySelector('[value="disk"]'); - folderOption.disabled = !canStream || target === "windows" || multi; - folderOption.textContent = target === "windows" ? "Save to folder · unavailable for Windows (DLL restrictions)" : multi ? "Save to folder · unavailable for batch output" : "Save to folder · uncompressed"; - const libraryBytes = release?.platforms[target]?.expandedBytes; - const selectedBytes = inspected ? sum(summaries.map(summary => target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes)) : null; - const conversionTooLarge = inspected && summaries.some(summary => (target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes) > 256 * 1024 * 1024); - const iconTooLarge = target === "windows" && inspected && summaries.some(summary => summary.windowsIconTooLarge); - const conversionBlocked = conversionTooLarge || iconTooLarge; - let estimate = null; - let estimateOverflow = false; - if (inspected && libraryBytes !== undefined) { - try { estimate = batchMemoryEstimate(summaries, target, libraryBytes); } - catch (error) { - if (!(error instanceof Error) || error.message !== "ZIP contains an unsupported file size") throw error; - estimateOverflow = true; - } - } - const tooLarge = estimateOverflow || (estimate !== null && estimate > 256 * 1024 * 1024); - $("save").querySelector('[value="memory"]').disabled = Boolean(tooLarge); - const memoryBlocked = $("save").value === "memory" && tooLarge; - if (inspected) { - const totalFiles = sum(summaries.map(summary => summary.files)); - const totalBytes = sum(summaries.map(summary => summary.totalBytes)); - const modules = sum(summaries.map(summary => summary.modules)); - const sizeText = value => value === null ? "beyond the supported size range" : bytes(value); - const countText = value => value === null ? "beyond the supported range" : value; - $("archive-info").textContent = multi - ? `${selectedInputs.length} apps · ${countText(totalFiles)} files · ${sizeText(totalBytes)} expanded · ${countText(modules)} modules · ${sizeText(selectedBytes)} ${target} conversion input` - : `${summaries[0].files} files · ${bytes(summaries[0].totalBytes)} expanded · ${summaries[0].modules} modules · ${bytes(target === "windows" ? summaries[0].windowsSelectedBytes : summaries[0].selectedBytes)} ${target} conversion input` + (summaries[0].root ? ` · Root: ${summaries[0].root}` : ""); - } - $("save-note").textContent = multi ? $("save").value === "memory" ? "Creates one ZIP with each app in its own folder. The complete bundle must fit in the 256 MiB memory-download budget." : "Writes one compressed ZIP containing each app in its own folder, without holding the whole bundle in memory." : $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Incomplete output folders are removed after cancellation or failure. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; - $("browser-note").hidden = supported && canStream && canSaveZip && !memoryBlocked && !folderBlocked && !conversionBlocked; - $("browser-note").textContent = !supported ? "This browser or connection does not support the converter. Use a current browser over HTTPS, or use the native relinker." : folderBlocked && multi ? "Batch conversion creates one ZIP bundle. Choose Save ZIP to disk or Download in memory." : folderBlocked ? "Browsers block direct DLL creation. Windows output requires ZIP. Choose Save ZIP to disk or Download in memory." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : estimateOverflow ? "The combined output exceeds the browser's supported size range. Choose Save ZIP to disk." : memoryBlocked ? "This output exceeds the memory-download limit. Choose Save ZIP to disk. If that option is unavailable, open this page in desktop Chrome or Edge." : "Some direct disk saving options are unavailable in this browser or context. Small apps can still be downloaded in memory. For larger apps, use desktop Chrome or Edge over HTTPS."; - const hasWarnings = inspected && summaries.some(summary => summary.warnings.length > 0); - $("convert").disabled = !supported || working || !inspected || !release || conversionBlocked || memoryBlocked || folderBlocked || (hasWarnings && !$("acknowledge").checked); - $("ready-note").textContent = inspected ? (folderBlocked && multi ? "Choose a ZIP save mode for the app bundle." : folderBlocked ? "Choose a ZIP save mode for Windows." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : memoryBlocked ? "A direct disk save is required for this output." : release ? `Libraries: ${release.tag} · ${multi ? "ZIP bundle" : $("save").value === "disk" ? "Uncompressed folder" : "ZIP compression level 9"}` : "Loading release information…") : "Choose one folder or one or more ZIP files to get started."; -} - -function lock(value) { - for (const id of ["choose", "choose-folder", "change", "target", "save", "intel"]) $(id).disabled = value || !supported || (id === "choose-folder" && !("webkitdirectory" in $("folder"))); -} - -async function fail(message) { - if (failing) return; - failing = true; - const report = failureReport({ - message, - phase: $("phase").textContent, - input: $("current-file").textContent, - target: document.querySelector('input[name="target"]:checked').value, - release, - log: logs - }); - worker?.terminate(); - worker = null; - working = true; - selectedInputs = []; - summaries = []; - inspectionComplete = false; - const transaction = outputTransaction; - outputTransaction = null; - outputDirectory = null; - if (transaction) { - $("status").hidden = false; - $("phase").textContent = "Removing incomplete output folder"; - } - const activeSink = sink; - sink = null; - for (const failure of await abortAndDiscardOutput(transaction, activeSink)) message += `\n${failure}`; - if (report === message) $("error").textContent = message; - else { - const details = document.createElement("details"); - const summary = document.createElement("summary"); - summary.textContent = "Failure details"; - const text = document.createElement("pre"); - text.textContent = report; - details.append(summary, text); - $("error").replaceChildren(document.createTextNode(message), details); - } - $("error").hidden = false; - $("status").hidden = true; - $("cancel").hidden = true; - $("drop").hidden = false; - $("selection").hidden = true; - $("warnings").hidden = true; - $("app-list").replaceChildren(); - $("file").value = ""; - working = false; - failing = false; - lock(false); - update(); -} - -async function onMessage({ data }) { - try { - if (data.type === "progress") { - if (data.message) { - logs.push(`${data.appName ? `${data.appName}: ` : ""}${data.message.trim()}`); - logs = logs.slice(-150); - $("log").textContent = logs.join("\n"); - return; - } - const phase = data.phase ?? stages[data.stage]; - $("phase").textContent = data.appCount > 1 ? `App ${data.appIndex + 1}/${data.appCount} · ${phase}` : phase; - $("current-file").textContent = [data.appName, data.input].filter(Boolean).join(" · "); - if (Number.isFinite(data.total) && data.total > 0) { - const percent = Math.min(100, Math.floor(data.completed / data.total * 100)); - $("progress").value = percent; - $("percent").textContent = `${percent}%`; - } else { - $("progress").removeAttribute("value"); - $("percent").textContent = ""; - } - } else if (data.type === "inspected") { - summaries[data.index] = data.summary; - } else if (data.type === "inspection-complete") { - inspectionComplete = true; - working = false; - $("selection").hidden = false; - $("drop").hidden = true; - $("archive-name").textContent = selectedInputs.length > 1 ? `${selectedInputs.length} app ZIPs` : selectedInputs[0].name; - $("app-list").replaceChildren(...selectedInputs.map((input, index) => { - const item = document.createElement("li"); - const name = document.createElement("strong"); - name.textContent = input.name; - const details = document.createElement("span"); - const summary = summaries[index]; - details.textContent = `${summary.files} files · ${bytes(summary.totalBytes)} expanded · ${summary.modules} modules`; - item.append(name, details); - return item; - })); - $("app-list").hidden = selectedInputs.length < 2; - const warningItems = []; - for (let index = 0; index < summaries.length; index++) { - for (const message of summaries[index].warnings) { - const item = document.createElement("li"); - item.textContent = selectedInputs.length > 1 ? `${selectedInputs[index].name}: ${message}` : message; - warningItems.push(item); - } - } - $("warning-list").replaceChildren(...warningItems); - $("warnings").hidden = warningItems.length === 0; - $("status").hidden = true; - $("cancel").hidden = true; - lock(false); - update(); - } else if (data.type === "directory" || data.type === "open-file") { - if (!outputDirectory) throw new Error("No output folder is open"); - const parts = data.name.replace(/\/$/u, "").split("/"); - if (parts.some(part => !part || part === "." || part === ".." || part.includes("\\"))) throw new Error("Invalid output path"); - const filename = data.type === "open-file" ? parts.pop() : null; - let parent = outputDirectory; - for (const part of parts) parent = await parent.getDirectoryHandle(part, { create: true }); - if (filename) { - try { - const handle = await parent.getFileHandle(filename, { create: true }); - sink = await handle.createWritable(); - } catch (error) { - throw new Error(`Cannot write ${data.name}: ${error.message}. Choose ZIP output if the browser blocks this file type.`); - } - } - worker?.postMessage({ type: "written" }); - } else if (data.type === "close-file") { - if (!sink) throw new Error("No output file is open"); - await sink.close(); - sink = null; - worker?.postMessage({ type: "written" }); - } else if (data.type === "write") { - if (!sink) throw new Error("No output file is open"); - await sink.write(data.bytes); - worker?.postMessage({ type: "written" }); - } else if (data.type === "complete") { - if (sink) { await sink.close(); sink = null; } - working = false; - $("done").hidden = false; - $("status").hidden = true; - $("cancel").hidden = true; - $("result-info").textContent = `${data.apps > 1 ? `${data.apps} apps · ` : ""}${bytes(data.size)} ${data.mode === "disk" ? "files" : "ZIP"} · ${data.release} system libraries · ${data.apps > 1 ? "combined app bundle" : data.outputName}`; - if (data.blob) { - downloadUrl = URL.createObjectURL(data.blob); - $("download").href = downloadUrl; - $("download").download = `${data.outputName}.zip`; - $("download").hidden = false; - } else if (data.mode === "zip") { - $("result-info").textContent += " · ZIP saved to disk"; - } else { - $("result-info").textContent += ` · Saved in ${outputTransaction.name}/`; - if (document.querySelector('input[name="target"]:checked').value === "linux") $("result-info").textContent += ` · On Linux, run chmod +x "${data.outputName}" before launching.`; - } - $("result-note").textContent = data.apps > 1 ? "Extract the ZIP; each app is in its own folder with its executable, libs and app0 files. On Linux, run chmod +x */*.elf before launching." : "Extract the entire ZIP before launching the executable."; - $("result-note").hidden = data.mode === "disk"; - outputDirectory = null; - outputTransaction?.commit(); - outputTransaction = null; - selectedInputs = []; - summaries = []; - inspectionComplete = false; - worker.terminate(); - worker = null; - lock(false); - update(); - } else if (data.type === "error") await fail(data.message); - } catch (error) { await fail(error.message); } -} - -async function selectInputs(inputs) { - if (working || !supported || !inputs.length) return; - if (inputs.some(input => !input.files && !input.name.toLowerCase().endsWith(".zip")) || (inputs.length > 1 && inputs.some(input => input.files))) { - await fail("Batch conversion accepts ZIP archives only. Choose one folder or select ZIP files."); - return; - } - worker?.terminate(); - if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } - selectedInputs = inputs; - summaries = []; - inspectionComplete = false; - working = true; - logs = []; - $("log").textContent = ""; - for (const id of ["error", "done", "download", "warnings", "selection"]) $(id).hidden = true; - $("acknowledge").checked = false; - $("status").hidden = false; - $("cancel").hidden = false; - $("phase").textContent = "Inspecting files"; - $("current-file").textContent = inputs.length > 1 ? `Inspecting ${inputs.length} ZIP archives` : inputs[0].name; - $("percent").textContent = ""; - $("progress").removeAttribute("value"); - lock(true); - update(); - worker = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" }); - worker.onmessage = onMessage; - worker.onerror = event => fail(event.message || "The converter worker stopped. Try a smaller app or use the native relinker."); - worker.postMessage({ type: "inspect", inputs }); -} - -$("choose").onclick = () => { $("file").value = ""; $("file").click(); }; -$("change").onclick = () => { - if (working) return; - worker?.terminate(); - worker = null; - if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } - selectedInputs = []; - summaries = []; - inspectionComplete = false; - $("file").value = ""; - $("folder").value = ""; - $("app-list").replaceChildren(); - for (const id of ["selection", "warnings", "error", "done", "download", "app-list"]) $(id).hidden = true; - $("acknowledge").checked = false; - $("drop").hidden = false; - update(); -}; -$("choose-folder").onclick = () => { $("folder").value = ""; $("folder").click(); }; -$("folder").onchange = () => { - const files = [...$("folder").files].map(file => ({ path: file.webkitRelativePath, file })); - if (files.length) selectInputs([{ name: files[0].path.split("/")[0], files }]); -}; -$("file").onchange = () => { - const inputs = [...$("file").files].map(file => ({ name: file.name, file })); - $("file").value = ""; - selectInputs(inputs); -}; -for (const type of ["dragenter", "dragover"]) $("drop").addEventListener(type, event => { event.preventDefault(); if (!working) $("drop").classList.add("dragging"); }); -$("drop").ondragleave = () => $("drop").classList.remove("dragging"); -$("drop").ondrop = event => { - event.preventDefault(); - $("drop").classList.remove("dragging"); - if (working) return; - selectInputs([...event.dataTransfer.files].map(file => ({ name: file.name, file }))); -}; -for (const id of ["save", "target", "acknowledge"]) $(id).addEventListener("change", update); -$("cancel").onclick = () => fail("Cancelled. Choose the ZIP or folder again to restart."); -$("convert").onclick = async () => { - if ($("convert").disabled) return; - working = true; - lock(true); - update(); - try { - const target = document.querySelector('input[name="target"]:checked').value; - if ($("save").value === "zip") { - const name = selectedInputs.length > 1 ? `anyps5-${target}-apps.zip` : `${selectedInputs[0].name.replace(/\.zip$/iu, "")}-${target}.zip`; - const handle = await window.showSaveFilePicker({ suggestedName: name, types: [{ description: "ZIP archive", accept: { "application/zip": [".zip"] } }] }); - sink = await handle.createWritable(); - } else if ($("save").value === "disk") { - const parent = await window.showDirectoryPicker({ mode: "readwrite" }); - const name = selectedInputs[0].name.replace(/\.zip$/iu, "") + "-" + target; - outputTransaction = await OutputDirectoryTransaction.create(parent, name); - outputDirectory = outputTransaction.directory; - } - $("status").hidden = false; - $("cancel").hidden = false; - $("error").hidden = true; - worker.postMessage({ type: "convert", target, toIntel: $("intel").checked, mode: $("save").value }); - } catch (error) { - if (error.name === "AbortError") { working = false; lock(false); update(); } - else await fail(error.message); - } -}; -$("theme").onclick = () => { - const dark = document.documentElement.dataset.theme ? document.documentElement.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches; - document.documentElement.dataset.theme = dark ? "light" : "dark"; -}; -update(); -lock(false); -if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status}).${response.status === 404 ? " release.json is generated during the site build, so it is missing when serving demo/web directly." : ""}`); return response.json(); }).then(value => { - if (!value.tag || !["windows", "linux"].every(target => Number.isSafeInteger(value.platforms?.[target]?.expandedBytes))) throw new Error("Invalid release information"); - release = value; - update(); -}).catch(error => fail(error.message)); - -if (!("webkitdirectory" in $("folder"))) { - $("choose-folder").disabled = true; - $("choose-folder").title = "Folder selection is unavailable in this browser. Choose a ZIP instead."; -} +import { abortAndDiscardOutput, OutputDirectoryTransaction } from "./output-directory.mjs"; +import { batchMemoryEstimate } from "./batch.mjs"; +import { failureReport } from "./failure-report.mjs"; + +const $ = id => document.getElementById(id); +const stages = ["Converting executable", "Converting PRX modules", "Scanning syscalls", "Scanning instructions", "Converting instructions", "Analysing imports", "Building output image", "Writing output"]; +let worker; +let selectedInputs = []; +let summaries = []; +let inspectionComplete = false; +let working = false; +let failing = false; +let sink; +let outputDirectory; +let outputTransaction; +let downloadUrl; +let logs = []; +let release; +const canSaveZip = typeof window.showSaveFilePicker === "function" && window.isSecureContext; +const canStream = typeof window.showDirectoryPicker === "function" && window.isSecureContext; +const supported = window.isSecureContext && window.Worker && window.WebAssembly && window.ReadableStream && window.WritableStream && window.crypto?.subtle; +$("save").querySelector('[value="disk"]').disabled = !canStream; +$("save").querySelector('[value="zip"]').disabled = !canSaveZip; +$("save").value = canSaveZip ? "zip" : "memory"; + +function bytes(value) { + if (value < 1024) return `${value} B`; + const unit = value >= 1024 ** 3 ? 3 : value >= 1024 ** 2 ? 2 : 1; + return `${(value / 1024 ** unit).toFixed(1)} ${["B", "KiB", "MiB", "GiB"][unit]}`; +} + +function sum(values) { + let total = 0; + for (const value of values) { + if (!Number.isSafeInteger(value) || value < 0 || total > Number.MAX_SAFE_INTEGER - value) return null; + total += value; + } + return total; +} + +function update() { + const target = document.querySelector('input[name="target"]:checked').value; + const multi = selectedInputs.length > 1; + const inspected = inspectionComplete && summaries.length === selectedInputs.length; + const folderBlocked = $("save").value === "disk" && (target === "windows" || multi); + const folderOption = $("save").querySelector('[value="disk"]'); + folderOption.disabled = !canStream || target === "windows" || multi; + folderOption.textContent = target === "windows" ? "Save to folder · unavailable for Windows (DLL restrictions)" : multi ? "Save to folder · unavailable for batch output" : "Save to folder · uncompressed"; + const libraryBytes = release?.platforms[target]?.expandedBytes; + const selectedBytes = inspected ? sum(summaries.map(summary => target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes)) : null; + const conversionTooLarge = inspected && summaries.some(summary => (target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes) > 256 * 1024 * 1024); + const iconTooLarge = target === "windows" && inspected && summaries.some(summary => summary.windowsIconTooLarge); + const conversionBlocked = conversionTooLarge || iconTooLarge; + let estimate = null; + let estimateOverflow = false; + if (inspected && libraryBytes !== undefined) { + try { estimate = batchMemoryEstimate(summaries, target, libraryBytes); } + catch (error) { + if (!(error instanceof Error) || error.message !== "ZIP contains an unsupported file size") throw error; + estimateOverflow = true; + } + } + const tooLarge = estimateOverflow || (estimate !== null && estimate > 256 * 1024 * 1024); + $("save").querySelector('[value="memory"]').disabled = Boolean(tooLarge); + const memoryBlocked = $("save").value === "memory" && tooLarge; + if (inspected) { + const totalFiles = sum(summaries.map(summary => summary.files)); + const totalBytes = sum(summaries.map(summary => summary.totalBytes)); + const modules = sum(summaries.map(summary => summary.modules)); + const sizeText = value => value === null ? "beyond the supported size range" : bytes(value); + const countText = value => value === null ? "beyond the supported range" : value; + $("archive-info").textContent = multi + ? `${selectedInputs.length} apps · ${countText(totalFiles)} files · ${sizeText(totalBytes)} expanded · ${countText(modules)} modules · ${sizeText(selectedBytes)} ${target} conversion input` + : `${summaries[0].files} files · ${bytes(summaries[0].totalBytes)} expanded · ${summaries[0].modules} modules · ${bytes(target === "windows" ? summaries[0].windowsSelectedBytes : summaries[0].selectedBytes)} ${target} conversion input` + (summaries[0].root ? ` · Root: ${summaries[0].root}` : ""); + } + $("save-note").textContent = multi ? $("save").value === "memory" ? "Creates one ZIP with each app in its own folder. The complete bundle must fit in the 256 MiB memory-download budget." : "Writes one compressed ZIP containing each app in its own folder, without holding the whole bundle in memory." : $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Incomplete output folders are removed after cancellation or failure. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; + $("browser-note").hidden = supported && canStream && canSaveZip && !memoryBlocked && !folderBlocked && !conversionBlocked; + $("browser-note").textContent = !supported ? "This browser or connection does not support the converter. Use a current browser over HTTPS, or use the native relinker." : folderBlocked && multi ? "Batch conversion creates one ZIP bundle. Choose Save ZIP to disk or Download in memory." : folderBlocked ? "Browsers block direct DLL creation. Windows output requires ZIP. Choose Save ZIP to disk or Download in memory." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : estimateOverflow ? "The combined output exceeds the browser's supported size range. Choose Save ZIP to disk." : memoryBlocked ? "This output exceeds the memory-download limit. Choose Save ZIP to disk. If that option is unavailable, open this page in desktop Chrome or Edge." : "Some direct disk saving options are unavailable in this browser or context. Small apps can still be downloaded in memory. For larger apps, use desktop Chrome or Edge over HTTPS."; + const hasWarnings = inspected && summaries.some(summary => summary.warnings.length > 0); + $("convert").disabled = !supported || working || !inspected || !release || conversionBlocked || memoryBlocked || folderBlocked || (hasWarnings && !$("acknowledge").checked); + $("ready-note").textContent = inspected ? (folderBlocked && multi ? "Choose a ZIP save mode for the app bundle." : folderBlocked ? "Choose a ZIP save mode for Windows." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : memoryBlocked ? "A direct disk save is required for this output." : release ? `Libraries: ${release.tag} · ${multi ? "ZIP bundle" : $("save").value === "disk" ? "Uncompressed folder" : "ZIP compression level 9"}` : "Loading release information…") : "Choose one folder or one or more ZIP files to get started."; +} + +function lock(value) { + for (const id of ["choose", "choose-folder", "change", "target", "save", "intel"]) $(id).disabled = value || !supported || (id === "choose-folder" && !("webkitdirectory" in $("folder"))); +} + +async function fail(message) { + if (failing) return; + failing = true; + const report = failureReport({ + message, + phase: $("phase").textContent, + input: $("current-file").textContent, + target: document.querySelector('input[name="target"]:checked').value, + release, + log: logs + }); + worker?.terminate(); + worker = null; + working = true; + selectedInputs = []; + summaries = []; + inspectionComplete = false; + const transaction = outputTransaction; + outputTransaction = null; + outputDirectory = null; + if (transaction) { + $("status").hidden = false; + $("phase").textContent = "Removing incomplete output folder"; + } + const activeSink = sink; + sink = null; + for (const failure of await abortAndDiscardOutput(transaction, activeSink)) message += `\n${failure}`; + if (report === message) $("error").textContent = message; + else { + const details = document.createElement("details"); + const summary = document.createElement("summary"); + summary.textContent = "Failure details"; + const text = document.createElement("pre"); + text.textContent = report; + details.append(summary, text); + $("error").replaceChildren(document.createTextNode(message), details); + } + $("error").hidden = false; + $("status").hidden = true; + $("cancel").hidden = true; + $("drop").hidden = false; + $("selection").hidden = true; + $("warnings").hidden = true; + $("app-list").replaceChildren(); + $("file").value = ""; + working = false; + failing = false; + lock(false); + update(); +} + +async function onMessage({ data }) { + try { + if (data.type === "progress") { + if (data.message) { + logs.push(`${data.appName ? `${data.appName}: ` : ""}${data.message.trim()}`); + logs = logs.slice(-150); + $("log").textContent = logs.join("\n"); + return; + } + const phase = data.phase ?? stages[data.stage]; + $("phase").textContent = data.appCount > 1 ? `App ${data.appIndex + 1}/${data.appCount} · ${phase}` : phase; + $("current-file").textContent = [data.appName, data.input].filter(Boolean).join(" · "); + if (Number.isFinite(data.total) && data.total > 0) { + const percent = Math.min(100, Math.floor(data.completed / data.total * 100)); + $("progress").value = percent; + $("percent").textContent = `${percent}%`; + } else { + $("progress").removeAttribute("value"); + $("percent").textContent = ""; + } + } else if (data.type === "inspected") { + summaries[data.index] = data.summary; + } else if (data.type === "inspection-complete") { + inspectionComplete = true; + working = false; + $("selection").hidden = false; + $("drop").hidden = true; + $("archive-name").textContent = selectedInputs.length > 1 ? `${selectedInputs.length} app ZIPs` : selectedInputs[0].name; + $("app-list").replaceChildren(...selectedInputs.map((input, index) => { + const item = document.createElement("li"); + const name = document.createElement("strong"); + name.textContent = input.name; + const details = document.createElement("span"); + const summary = summaries[index]; + details.textContent = `${summary.files} files · ${bytes(summary.totalBytes)} expanded · ${summary.modules} modules`; + item.append(name, details); + return item; + })); + $("app-list").hidden = selectedInputs.length < 2; + const warningItems = []; + for (let index = 0; index < summaries.length; index++) { + for (const message of summaries[index].warnings) { + const item = document.createElement("li"); + item.textContent = selectedInputs.length > 1 ? `${selectedInputs[index].name}: ${message}` : message; + warningItems.push(item); + } + } + $("warning-list").replaceChildren(...warningItems); + $("warnings").hidden = warningItems.length === 0; + $("status").hidden = true; + $("cancel").hidden = true; + lock(false); + update(); + } else if (data.type === "directory" || data.type === "open-file") { + if (!outputDirectory) throw new Error("No output folder is open"); + const parts = data.name.replace(/\/$/u, "").split("/"); + if (parts.some(part => !part || part === "." || part === ".." || part.includes("\\"))) throw new Error("Invalid output path"); + const filename = data.type === "open-file" ? parts.pop() : null; + let parent = outputDirectory; + for (const part of parts) parent = await parent.getDirectoryHandle(part, { create: true }); + if (filename) { + try { + const handle = await parent.getFileHandle(filename, { create: true }); + sink = await handle.createWritable(); + } catch (error) { + throw new Error(`Cannot write ${data.name}: ${error.message}. Choose ZIP output if the browser blocks this file type.`); + } + } + worker?.postMessage({ type: "written" }); + } else if (data.type === "close-file") { + if (!sink) throw new Error("No output file is open"); + await sink.close(); + sink = null; + worker?.postMessage({ type: "written" }); + } else if (data.type === "write") { + if (!sink) throw new Error("No output file is open"); + await sink.write(data.bytes); + worker?.postMessage({ type: "written" }); + } else if (data.type === "complete") { + if (sink) { await sink.close(); sink = null; } + working = false; + $("done").hidden = false; + $("status").hidden = true; + $("cancel").hidden = true; + $("result-info").textContent = `${data.apps > 1 ? `${data.apps} apps · ` : ""}${bytes(data.size)} ${data.mode === "disk" ? "files" : "ZIP"} · ${data.release} system libraries · ${data.apps > 1 ? "combined app bundle" : data.outputName}`; + if (data.blob) { + downloadUrl = URL.createObjectURL(data.blob); + $("download").href = downloadUrl; + $("download").download = `${data.outputName}.zip`; + $("download").hidden = false; + } else if (data.mode === "zip") { + $("result-info").textContent += " · ZIP saved to disk"; + } else { + $("result-info").textContent += ` · Saved in ${outputTransaction.name}/`; + if (document.querySelector('input[name="target"]:checked').value === "linux") $("result-info").textContent += ` · On Linux, run chmod +x "${data.outputName}" before launching.`; + } + $("result-note").textContent = data.apps > 1 ? "Extract the ZIP; each app is in its own folder with its executable, libs and app0 files. On Linux, run chmod +x */*.elf before launching." : "Extract the entire ZIP before launching the executable."; + $("result-note").hidden = data.mode === "disk"; + outputDirectory = null; + outputTransaction?.commit(); + outputTransaction = null; + selectedInputs = []; + summaries = []; + inspectionComplete = false; + worker.terminate(); + worker = null; + lock(false); + update(); + } else if (data.type === "error") await fail(data.message); + } catch (error) { await fail(error.message); } +} + +async function selectInputs(inputs) { + if (working || !supported || !inputs.length) return; + if (inputs.some(input => !input.files && !input.name.toLowerCase().endsWith(".zip")) || (inputs.length > 1 && inputs.some(input => input.files))) { + await fail("Batch conversion accepts ZIP archives only. Choose one folder or select ZIP files."); + return; + } + worker?.terminate(); + if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } + selectedInputs = inputs; + summaries = []; + inspectionComplete = false; + working = true; + logs = []; + $("log").textContent = ""; + for (const id of ["error", "done", "download", "warnings", "selection"]) $(id).hidden = true; + $("acknowledge").checked = false; + $("status").hidden = false; + $("cancel").hidden = false; + $("phase").textContent = "Inspecting files"; + $("current-file").textContent = inputs.length > 1 ? `Inspecting ${inputs.length} ZIP archives` : inputs[0].name; + $("percent").textContent = ""; + $("progress").removeAttribute("value"); + lock(true); + update(); + worker = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" }); + worker.onmessage = onMessage; + worker.onerror = event => fail(event.message || "The converter worker stopped. Try a smaller app or use the native relinker."); + worker.postMessage({ type: "inspect", inputs }); +} + +$("choose").onclick = () => { $("file").value = ""; $("file").click(); }; +$("change").onclick = () => { + if (working) return; + worker?.terminate(); + worker = null; + if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } + selectedInputs = []; + summaries = []; + inspectionComplete = false; + $("file").value = ""; + $("folder").value = ""; + $("app-list").replaceChildren(); + for (const id of ["selection", "warnings", "error", "done", "download", "app-list"]) $(id).hidden = true; + $("acknowledge").checked = false; + $("drop").hidden = false; + update(); +}; +$("choose-folder").onclick = () => { $("folder").value = ""; $("folder").click(); }; +$("folder").onchange = () => { + const files = [...$("folder").files].map(file => ({ path: file.webkitRelativePath, file })); + if (files.length) selectInputs([{ name: files[0].path.split("/")[0], files }]); +}; +$("file").onchange = () => { + const inputs = [...$("file").files].map(file => ({ name: file.name, file })); + $("file").value = ""; + selectInputs(inputs); +}; +for (const type of ["dragenter", "dragover"]) $("drop").addEventListener(type, event => { event.preventDefault(); if (!working) $("drop").classList.add("dragging"); }); +$("drop").ondragleave = () => $("drop").classList.remove("dragging"); +$("drop").ondrop = event => { + event.preventDefault(); + $("drop").classList.remove("dragging"); + if (working) return; + selectInputs([...event.dataTransfer.files].map(file => ({ name: file.name, file }))); +}; +for (const id of ["save", "target", "acknowledge"]) $(id).addEventListener("change", update); +$("cancel").onclick = () => fail("Cancelled. Choose the ZIP or folder again to restart."); +$("convert").onclick = async () => { + if ($("convert").disabled) return; + working = true; + lock(true); + update(); + try { + const target = document.querySelector('input[name="target"]:checked').value; + if ($("save").value === "zip") { + const name = selectedInputs.length > 1 ? `anyps5-${target}-apps.zip` : `${selectedInputs[0].name.replace(/\.zip$/iu, "")}-${target}.zip`; + const handle = await window.showSaveFilePicker({ suggestedName: name, types: [{ description: "ZIP archive", accept: { "application/zip": [".zip"] } }] }); + sink = await handle.createWritable(); + } else if ($("save").value === "disk") { + const parent = await window.showDirectoryPicker({ mode: "readwrite" }); + const name = selectedInputs[0].name.replace(/\.zip$/iu, "") + "-" + target; + outputTransaction = await OutputDirectoryTransaction.create(parent, name); + outputDirectory = outputTransaction.directory; + } + $("status").hidden = false; + $("cancel").hidden = false; + $("error").hidden = true; + worker.postMessage({ type: "convert", target, toIntel: $("intel").checked, mode: $("save").value }); + } catch (error) { + if (error.name === "AbortError") { working = false; lock(false); update(); } + else await fail(error.message); + } +}; +$("theme").onclick = () => { + const dark = document.documentElement.dataset.theme ? document.documentElement.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches; + document.documentElement.dataset.theme = dark ? "light" : "dark"; +}; +update(); +lock(false); +if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status}).${response.status === 404 ? " release.json is generated during the site build, so it is missing when serving demo/web directly." : ""}`); return response.json(); }).then(value => { + if (!value.tag || !["windows", "linux"].every(target => Number.isSafeInteger(value.platforms?.[target]?.expandedBytes))) throw new Error("Invalid release information"); + release = value; + update(); +}).catch(error => fail(error.message)); + +if (!("webkitdirectory" in $("folder"))) { + $("choose-folder").disabled = true; + $("choose-folder").title = "Folder selection is unavailable in this browser. Choose a ZIP instead."; +} From 2e2e6f277540f403c8c502ff3c09160a6feb6f95 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:25:07 +0100 Subject: [PATCH 363/702] feat(libSceIcu): declare 150 PS5 3.20 exports --- core/libs/prx/libSceIcu/Export.cpp | 751 ++++++++++++++++++++++++++++- docs/dev/debt/libSceIcu.md | 4 + 2 files changed, 754 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceIcu/Export.cpp b/core/libs/prx/libSceIcu/Export.cpp index d81f4a5e32..9aef8049dd 100644 --- a/core/libs/prx/libSceIcu/Export.cpp +++ b/core/libs/prx/libSceIcu/Export.cpp @@ -5,6 +5,755 @@ extern "C" { -APS5_DUMMY_FUN + +int APS5_VABI sceIcuUnknown_2m4vg20UoXI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2m4vg20UoXI", sceIcuUnknown_2m4vg20UoXI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_wwOr3NA7FRc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wwOr3NA7FRc", sceIcuUnknown_wwOr3NA7FRc_nid_no_patch); + +int APS5_VABI sceIcuUnknown_RGVOIIycuyA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RGVOIIycuyA", sceIcuUnknown_RGVOIIycuyA_nid_no_patch); + +int APS5_VABI sceIcuUnknown_9duJwhiYGCE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("9duJwhiYGCE", sceIcuUnknown_9duJwhiYGCE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_wuiJZ6Iy6Mc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wuiJZ6Iy6Mc", sceIcuUnknown_wuiJZ6Iy6Mc_nid_no_patch); + +int APS5_VABI sceIcuUnknown_VZFw_xSe6Mo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("VZFw+xSe6Mo", sceIcuUnknown_VZFw_xSe6Mo_nid_no_patch); + +int APS5_VABI sceIcuUnknown_uKBkvN84hbw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uKBkvN84hbw", sceIcuUnknown_uKBkvN84hbw_nid_no_patch); + +int APS5_VABI sceIcuUnknown_JKRf_2yrIaE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JKRf+2yrIaE", sceIcuUnknown_JKRf_2yrIaE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_4Gv3tXZ2WKI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4Gv3tXZ2WKI", sceIcuUnknown_4Gv3tXZ2WKI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_RFnKlYsiIb0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RFnKlYsiIb0", sceIcuUnknown_RFnKlYsiIb0_nid_no_patch); + +int APS5_VABI sceIcuUnknown_BZY5mpL0Syk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BZY5mpL0Syk", sceIcuUnknown_BZY5mpL0Syk_nid_no_patch); + +int APS5_VABI sceIcuUnknown_WLoOrEfsob4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WLoOrEfsob4", sceIcuUnknown_WLoOrEfsob4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_H33ge_SDjlI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("H33ge+SDjlI", sceIcuUnknown_H33ge_SDjlI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_wlK146pHnQ4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wlK146pHnQ4", sceIcuUnknown_wlK146pHnQ4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_QO5iuHMTu2o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QO5iuHMTu2o", sceIcuUnknown_QO5iuHMTu2o_nid_no_patch); + +int APS5_VABI sceIcuUnknown_9TndABWg5Ds_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("9TndABWg5Ds", sceIcuUnknown_9TndABWg5Ds_nid_no_patch); + +int APS5_VABI sceIcuUnknown_tcf9HUwNaX0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tcf9HUwNaX0", sceIcuUnknown_tcf9HUwNaX0_nid_no_patch); + +int APS5_VABI sceIcuUnknown_rbyBjvpNuME_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("rbyBjvpNuME", sceIcuUnknown_rbyBjvpNuME_nid_no_patch); + +int APS5_VABI sceIcuUnknown_4JJDz6r_ghE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4JJDz6r-ghE", sceIcuUnknown_4JJDz6r_ghE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_WPrktCwd_iw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WPrktCwd+iw", sceIcuUnknown_WPrktCwd_iw_nid_no_patch); + +int APS5_VABI sceIcuUnknown_sP5rfVNdvWM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sP5rfVNdvWM", sceIcuUnknown_sP5rfVNdvWM_nid_no_patch); + +int APS5_VABI sceIcuUnknown_c6eD2eDESGk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("c6eD2eDESGk", sceIcuUnknown_c6eD2eDESGk_nid_no_patch); + +int APS5_VABI sceIcuUnknown_8y7DjDBJ6Nw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8y7DjDBJ6Nw", sceIcuUnknown_8y7DjDBJ6Nw_nid_no_patch); + +int APS5_VABI sceIcuUnknown_b7p2qTtZPno_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("b7p2qTtZPno", sceIcuUnknown_b7p2qTtZPno_nid_no_patch); + +int APS5_VABI sceIcuUnknown_RlZwfEuRDx8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RlZwfEuRDx8", sceIcuUnknown_RlZwfEuRDx8_nid_no_patch); + +int APS5_VABI sceIcuUnknown_ZFJmsiUqcBI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZFJmsiUqcBI", sceIcuUnknown_ZFJmsiUqcBI_nid_no_patch); + +int APS5_VABI sceIcuUnknown__xsbcqOIJmM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+xsbcqOIJmM", sceIcuUnknown__xsbcqOIJmM_nid_no_patch); + +int APS5_VABI sceIcuUnknown_od0otGJ2Z3A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("od0otGJ2Z3A", sceIcuUnknown_od0otGJ2Z3A_nid_no_patch); + +int APS5_VABI sceIcuUnknown_vXu991uXNUo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("vXu991uXNUo", sceIcuUnknown_vXu991uXNUo_nid_no_patch); + +int APS5_VABI sceIcuUnknown_aF_oEFPzEsY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("aF+oEFPzEsY", sceIcuUnknown_aF_oEFPzEsY_nid_no_patch); + +int APS5_VABI sceIcuUnknown_lNPpoUVElSA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lNPpoUVElSA", sceIcuUnknown_lNPpoUVElSA_nid_no_patch); + +int APS5_VABI sceIcuUnknown_BTFjoSWI5_Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BTFjoSWI5-Y", sceIcuUnknown_BTFjoSWI5_Y_nid_no_patch); + +int APS5_VABI sceIcuUnknown_Q8P3U9F8p_Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Q8P3U9F8p-Y", sceIcuUnknown_Q8P3U9F8p_Y_nid_no_patch); + +int APS5_VABI sceIcuUnknown_4Kf0Hy8xJrI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4Kf0Hy8xJrI", sceIcuUnknown_4Kf0Hy8xJrI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_JGkExZI3fFI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JGkExZI3fFI", sceIcuUnknown_JGkExZI3fFI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_0cYEtBUJ5zQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0cYEtBUJ5zQ", sceIcuUnknown_0cYEtBUJ5zQ_nid_no_patch); + +int APS5_VABI sceIcuUnknown_nOadKT6rn7U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("nOadKT6rn7U", sceIcuUnknown_nOadKT6rn7U_nid_no_patch); + +int APS5_VABI sceIcuUnknown_1SsridDRBYQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1SsridDRBYQ", sceIcuUnknown_1SsridDRBYQ_nid_no_patch); + +int APS5_VABI sceIcuUnknown_71sgC7_39is_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("71sgC7+39is", sceIcuUnknown_71sgC7_39is_nid_no_patch); + +int APS5_VABI sceIcuUnknown_a25spvWYEOA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("a25spvWYEOA", sceIcuUnknown_a25spvWYEOA_nid_no_patch); + +int APS5_VABI sceIcuUnknown_EuFvqF73SwU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EuFvqF73SwU", sceIcuUnknown_EuFvqF73SwU_nid_no_patch); + +int APS5_VABI sceIcuUnknown_wybPkXuWnoE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wybPkXuWnoE", sceIcuUnknown_wybPkXuWnoE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_cY8dSl6Rqr4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cY8dSl6Rqr4", sceIcuUnknown_cY8dSl6Rqr4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_2qAOcgRJZ3M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2qAOcgRJZ3M", sceIcuUnknown_2qAOcgRJZ3M_nid_no_patch); + +int APS5_VABI sceIcuUnknown_n4yiKNo1yCs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("n4yiKNo1yCs", sceIcuUnknown_n4yiKNo1yCs_nid_no_patch); + +int APS5_VABI sceIcuUnknown_hI8JVrcGWhc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hI8JVrcGWhc", sceIcuUnknown_hI8JVrcGWhc_nid_no_patch); + +int APS5_VABI sceIcuUnknown_h16q7HjVyTE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("h16q7HjVyTE", sceIcuUnknown_h16q7HjVyTE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_Q8Y0ntVp_GY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Q8Y0ntVp-GY", sceIcuUnknown_Q8Y0ntVp_GY_nid_no_patch); + +int APS5_VABI sceIcuUnknown_TXEHKpCJ_aE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("TXEHKpCJ-aE", sceIcuUnknown_TXEHKpCJ_aE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_UmNS0P0_ovs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UmNS0P0-ovs", sceIcuUnknown_UmNS0P0_ovs_nid_no_patch); + +int APS5_VABI sceIcuUnknown_ndOe3Wc7xjA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ndOe3Wc7xjA", sceIcuUnknown_ndOe3Wc7xjA_nid_no_patch); + +int APS5_VABI sceIcuUnknown_r6Wuu1Tc438_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("r6Wuu1Tc438", sceIcuUnknown_r6Wuu1Tc438_nid_no_patch); + +int APS5_VABI sceIcuUnknown__kd2qcTMvHE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+kd2qcTMvHE", sceIcuUnknown__kd2qcTMvHE_nid_no_patch); + +int APS5_VABI sceIcuUnknown__aK6d418l4c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-aK6d418l4c", sceIcuUnknown__aK6d418l4c_nid_no_patch); + +int APS5_VABI sceIcuUnknown_5V2WIPOZcaA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5V2WIPOZcaA", sceIcuUnknown_5V2WIPOZcaA_nid_no_patch); + +int APS5_VABI sceIcuUnknown_FHSNfxQFEdA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("FHSNfxQFEdA", sceIcuUnknown_FHSNfxQFEdA_nid_no_patch); + +int APS5_VABI sceIcuUnknown_eD76mHY99fM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("eD76mHY99fM", sceIcuUnknown_eD76mHY99fM_nid_no_patch); + +int APS5_VABI sceIcuUnknown_F_2mXPmh_7s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("F+2mXPmh-7s", sceIcuUnknown_F_2mXPmh_7s_nid_no_patch); + +int APS5_VABI sceIcuUnknown_BQtUj7CVrC0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BQtUj7CVrC0", sceIcuUnknown_BQtUj7CVrC0_nid_no_patch); + +int APS5_VABI sceIcuUnknown_pCh4kIKMRhY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pCh4kIKMRhY", sceIcuUnknown_pCh4kIKMRhY_nid_no_patch); + +int APS5_VABI sceIcuUnknown_3Z22ZB8mHq8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3Z22ZB8mHq8", sceIcuUnknown_3Z22ZB8mHq8_nid_no_patch); + +int APS5_VABI sceIcuUnknown_Hka9gz6FuDs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Hka9gz6FuDs", sceIcuUnknown_Hka9gz6FuDs_nid_no_patch); + +int APS5_VABI sceIcuUnknown_5__ZFSRQ0_k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5+-ZFSRQ0+k", sceIcuUnknown_5__ZFSRQ0_k_nid_no_patch); + +int APS5_VABI sceIcuUnknown_zGuhKwXM9I0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zGuhKwXM9I0", sceIcuUnknown_zGuhKwXM9I0_nid_no_patch); + +int APS5_VABI sceIcuUnknown_iRm8tkU1ir4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("iRm8tkU1ir4", sceIcuUnknown_iRm8tkU1ir4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_6y_NITWLmmE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6y+NITWLmmE", sceIcuUnknown_6y_NITWLmmE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_qXSNbAnlsKg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qXSNbAnlsKg", sceIcuUnknown_qXSNbAnlsKg_nid_no_patch); + +int APS5_VABI sceIcuUnknown_u8WyPl4r3Oc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("u8WyPl4r3Oc", sceIcuUnknown_u8WyPl4r3Oc_nid_no_patch); + +int APS5_VABI sceIcuUnknown_7E_h3yiZjKw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("7E+h3yiZjKw", sceIcuUnknown_7E_h3yiZjKw_nid_no_patch); + +int APS5_VABI sceIcuUnknown_dzbKmf3Ycog_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dzbKmf3Ycog", sceIcuUnknown_dzbKmf3Ycog_nid_no_patch); + +int APS5_VABI sceIcuUnknown_AvrA9K2lvC4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AvrA9K2lvC4", sceIcuUnknown_AvrA9K2lvC4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_58CIUaViW_k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("58CIUaViW+k", sceIcuUnknown_58CIUaViW_k_nid_no_patch); + +int APS5_VABI sceIcuUnknown_mY5RbYVLzr4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mY5RbYVLzr4", sceIcuUnknown_mY5RbYVLzr4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_1GkIKiuqgBg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1GkIKiuqgBg", sceIcuUnknown_1GkIKiuqgBg_nid_no_patch); + +int APS5_VABI sceIcuUnknown_qGIX2VTcNJo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qGIX2VTcNJo", sceIcuUnknown_qGIX2VTcNJo_nid_no_patch); + +int APS5_VABI sceIcuUnknown_OJ_13SCOxgI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OJ+13SCOxgI", sceIcuUnknown_OJ_13SCOxgI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_wxirhbbuv84_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wxirhbbuv84", sceIcuUnknown_wxirhbbuv84_nid_no_patch); + +int APS5_VABI sceIcuUnknown_U089pStD8Bw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("U089pStD8Bw", sceIcuUnknown_U089pStD8Bw_nid_no_patch); + +int APS5_VABI sceIcuUnknown_O3zT1gO1ZNM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("O3zT1gO1ZNM", sceIcuUnknown_O3zT1gO1ZNM_nid_no_patch); + +int APS5_VABI sceIcuUnknown__L1K53Ku3mE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+L1K53Ku3mE", sceIcuUnknown__L1K53Ku3mE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_LtBBDJEZXhY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LtBBDJEZXhY", sceIcuUnknown_LtBBDJEZXhY_nid_no_patch); + +int APS5_VABI sceIcuUnknown_Sv7TbVAEqeQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Sv7TbVAEqeQ", sceIcuUnknown_Sv7TbVAEqeQ_nid_no_patch); + +int APS5_VABI sceIcuUnknown_twrAIl4N_EM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("twrAIl4N+EM", sceIcuUnknown_twrAIl4N_EM_nid_no_patch); + +int APS5_VABI sceIcuUnknown_n4o8mgXtF5s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("n4o8mgXtF5s", sceIcuUnknown_n4o8mgXtF5s_nid_no_patch); + +int APS5_VABI sceIcuUnknown__P5TDFU2_fc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+P5TDFU2-fc", sceIcuUnknown__P5TDFU2_fc_nid_no_patch); + +int APS5_VABI sceIcuUnknown_XVQ3cf0BdQ0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XVQ3cf0BdQ0", sceIcuUnknown_XVQ3cf0BdQ0_nid_no_patch); + +int APS5_VABI sceIcuUnknown_HneyXNQpPhQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HneyXNQpPhQ", sceIcuUnknown_HneyXNQpPhQ_nid_no_patch); + +int APS5_VABI sceIcuUnknown_bgnHFMqxYag_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bgnHFMqxYag", sceIcuUnknown_bgnHFMqxYag_nid_no_patch); + +int APS5_VABI sceIcuUnknown_LXxEsOsIa5g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LXxEsOsIa5g", sceIcuUnknown_LXxEsOsIa5g_nid_no_patch); + +int APS5_VABI sceIcuUnknown_Z5_W18jAIH4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Z5+W18jAIH4", sceIcuUnknown_Z5_W18jAIH4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_tcSjvPP2_4s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tcSjvPP2-4s", sceIcuUnknown_tcSjvPP2_4s_nid_no_patch); + +int APS5_VABI sceIcuUnknown_BhCcIlyHMrk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BhCcIlyHMrk", sceIcuUnknown_BhCcIlyHMrk_nid_no_patch); + +int APS5_VABI sceIcuUnknown_S0WDjcZLdqI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("S0WDjcZLdqI", sceIcuUnknown_S0WDjcZLdqI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_udAPjod3tpU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("udAPjod3tpU", sceIcuUnknown_udAPjod3tpU_nid_no_patch); + +int APS5_VABI sceIcuUnknown_CfP4Q2Yjm1U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("CfP4Q2Yjm1U", sceIcuUnknown_CfP4Q2Yjm1U_nid_no_patch); + +int APS5_VABI sceIcuUnknown_5UagplYjXBo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5UagplYjXBo", sceIcuUnknown_5UagplYjXBo_nid_no_patch); + +int APS5_VABI sceIcuUnknown_pkGTJH2IstM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pkGTJH2IstM", sceIcuUnknown_pkGTJH2IstM_nid_no_patch); + +int APS5_VABI sceIcuUnknown_zx0jGj_nSIM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zx0jGj+nSIM", sceIcuUnknown_zx0jGj_nSIM_nid_no_patch); + +int APS5_VABI sceIcuUnknown_b5KdXXbihxE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("b5KdXXbihxE", sceIcuUnknown_b5KdXXbihxE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_xHVUdb5NNSw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xHVUdb5NNSw", sceIcuUnknown_xHVUdb5NNSw_nid_no_patch); + +int APS5_VABI sceIcuUnknown_uAMs0e65nCk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uAMs0e65nCk", sceIcuUnknown_uAMs0e65nCk_nid_no_patch); + +int APS5_VABI sceIcuUnknown_zqQWBy7O_00_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zqQWBy7O-00", sceIcuUnknown_zqQWBy7O_00_nid_no_patch); + +int APS5_VABI sceIcuUnknown_OEiVDwLURmI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OEiVDwLURmI", sceIcuUnknown_OEiVDwLURmI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_ojLjExhl_hE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ojLjExhl-hE", sceIcuUnknown_ojLjExhl_hE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_sQ0avkeBLo4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sQ0avkeBLo4", sceIcuUnknown_sQ0avkeBLo4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_yVobQd4t_iA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yVobQd4t+iA", sceIcuUnknown_yVobQd4t_iA_nid_no_patch); + +int APS5_VABI sceIcuUnknown_FNfP9dKTd5Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("FNfP9dKTd5Q", sceIcuUnknown_FNfP9dKTd5Q_nid_no_patch); + +int APS5_VABI sceIcuUnknown_TU6Yl8_mT3c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("TU6Yl8+mT3c", sceIcuUnknown_TU6Yl8_mT3c_nid_no_patch); + +int APS5_VABI sceIcuUnknown_Wm0y_9IRE8I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Wm0y-9IRE8I", sceIcuUnknown_Wm0y_9IRE8I_nid_no_patch); + +int APS5_VABI sceIcuUnknown_xAo5lvA_W3o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xAo5lvA+W3o", sceIcuUnknown_xAo5lvA_W3o_nid_no_patch); + +int APS5_VABI sceIcuUnknown_KzcrFzKpYE8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KzcrFzKpYE8", sceIcuUnknown_KzcrFzKpYE8_nid_no_patch); + +int APS5_VABI sceIcuUnknown_pgvyZZ_d1ck_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pgvyZZ+d1ck", sceIcuUnknown_pgvyZZ_d1ck_nid_no_patch); + +int APS5_VABI sceIcuUnknown_TNZrG2iXikw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("TNZrG2iXikw", sceIcuUnknown_TNZrG2iXikw_nid_no_patch); + +int APS5_VABI sceIcuUnknown__t8PVjghGo4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-t8PVjghGo4", sceIcuUnknown__t8PVjghGo4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_HK0gjeXo2eo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HK0gjeXo2eo", sceIcuUnknown_HK0gjeXo2eo_nid_no_patch); + +int APS5_VABI sceIcuUnknown_Z5ljxAg3thw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Z5ljxAg3thw", sceIcuUnknown_Z5ljxAg3thw_nid_no_patch); + +int APS5_VABI sceIcuUnknown_OAFz3KoG_gA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OAFz3KoG-gA", sceIcuUnknown_OAFz3KoG_gA_nid_no_patch); + +int APS5_VABI sceIcuUnknown_6_NsFXHpWXc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6+NsFXHpWXc", sceIcuUnknown_6_NsFXHpWXc_nid_no_patch); + +int APS5_VABI sceIcuUnknown_q3mX36AQeC4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("q3mX36AQeC4", sceIcuUnknown_q3mX36AQeC4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_8XjRlb2loGU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8XjRlb2loGU", sceIcuUnknown_8XjRlb2loGU_nid_no_patch); + +int APS5_VABI sceIcuUnknown_kfOVuKHkkV4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("kfOVuKHkkV4", sceIcuUnknown_kfOVuKHkkV4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_2AqqzZ9fKwY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2AqqzZ9fKwY", sceIcuUnknown_2AqqzZ9fKwY_nid_no_patch); + +int APS5_VABI sceIcuUnknown_4h4kYUxRDgU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4h4kYUxRDgU", sceIcuUnknown_4h4kYUxRDgU_nid_no_patch); + +int APS5_VABI sceIcuUnknown_RIcNJWDYKhM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RIcNJWDYKhM", sceIcuUnknown_RIcNJWDYKhM_nid_no_patch); + +int APS5_VABI sceIcuUnknown_4FrZfvvYHCI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4FrZfvvYHCI", sceIcuUnknown_4FrZfvvYHCI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_ZxnyBy8reV4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZxnyBy8reV4", sceIcuUnknown_ZxnyBy8reV4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_X7gbPHVoNEw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("X7gbPHVoNEw", sceIcuUnknown_X7gbPHVoNEw_nid_no_patch); + +int APS5_VABI sceIcuUnknown_23uLv15jvFA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("23uLv15jvFA", sceIcuUnknown_23uLv15jvFA_nid_no_patch); + +int APS5_VABI sceIcuUnknown_MSr9DE8l0Sc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("MSr9DE8l0Sc", sceIcuUnknown_MSr9DE8l0Sc_nid_no_patch); + +int APS5_VABI sceIcuUnknown_aXUj3BT58vY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("aXUj3BT58vY", sceIcuUnknown_aXUj3BT58vY_nid_no_patch); + +int APS5_VABI sceIcuUnknown_FCx2DacLDWs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("FCx2DacLDWs", sceIcuUnknown_FCx2DacLDWs_nid_no_patch); + +int APS5_VABI sceIcuUnknown_W7SNhSZ_YdI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("W7SNhSZ-YdI", sceIcuUnknown_W7SNhSZ_YdI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_4g6BoFF_ywE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4g6BoFF-ywE", sceIcuUnknown_4g6BoFF_ywE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_vRW6XDP_8J4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("vRW6XDP-8J4", sceIcuUnknown_vRW6XDP_8J4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_qhWR6O_Zp_U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qhWR6O+Zp+U", sceIcuUnknown_qhWR6O_Zp_U_nid_no_patch); + +int APS5_VABI sceIcuUnknown_Zkuj3yHaXQQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Zkuj3yHaXQQ", sceIcuUnknown_Zkuj3yHaXQQ_nid_no_patch); + +int APS5_VABI sceIcuUnknown_WScz3_NnifQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WScz3+NnifQ", sceIcuUnknown_WScz3_NnifQ_nid_no_patch); + +int APS5_VABI sceIcuUnknown_rjnCmUev3aU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("rjnCmUev3aU", sceIcuUnknown_rjnCmUev3aU_nid_no_patch); + +int APS5_VABI sceIcuUnknown_2vIP9cBK1R0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2vIP9cBK1R0", sceIcuUnknown_2vIP9cBK1R0_nid_no_patch); + +int APS5_VABI sceIcuUnknown_ekxUEjhuk3Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ekxUEjhuk3Q", sceIcuUnknown_ekxUEjhuk3Q_nid_no_patch); + +int APS5_VABI sceIcuUnknown_JwGa8TaETwI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JwGa8TaETwI", sceIcuUnknown_JwGa8TaETwI_nid_no_patch); + +int APS5_VABI sceIcuUnknown_H4haJKBliZE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("H4haJKBliZE", sceIcuUnknown_H4haJKBliZE_nid_no_patch); + +int APS5_VABI sceIcuUnknown_ikElnlIFyWs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ikElnlIFyWs", sceIcuUnknown_ikElnlIFyWs_nid_no_patch); + +int APS5_VABI sceIcuUnknown_RR1fhpBoZ7w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RR1fhpBoZ7w", sceIcuUnknown_RR1fhpBoZ7w_nid_no_patch); + +int APS5_VABI sceIcuUnknown_yt7hn3QeBvY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yt7hn3QeBvY", sceIcuUnknown_yt7hn3QeBvY_nid_no_patch); + +int APS5_VABI sceIcuUnknown_i4FsTbO__x8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("i4FsTbO-+x8", sceIcuUnknown_i4FsTbO__x8_nid_no_patch); + +int APS5_VABI sceIcuUnknown_ebmFXXx04K4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ebmFXXx04K4", sceIcuUnknown_ebmFXXx04K4_nid_no_patch); + +int APS5_VABI sceIcuUnknown_BsjbIVU27jQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BsjbIVU27jQ", sceIcuUnknown_BsjbIVU27jQ_nid_no_patch); + +int APS5_VABI sceIcuUnknown_1YDkzlcc8YY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1YDkzlcc8YY", sceIcuUnknown_1YDkzlcc8YY_nid_no_patch); + +int APS5_VABI sceIcuUnknown_wezXBwKdjAY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wezXBwKdjAY", sceIcuUnknown_wezXBwKdjAY_nid_no_patch); } diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index dcf66e6027..63a026f939 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -3,3 +3,7 @@ ### Functional - [Invariant character and version helpers](../../../core/libs/prx/libSceIcu/Version.cpp) are unverified on a console. `u_charsToUChars`, `u_UCharsToChars`, `u_versionFromString`, `u_versionFromUString` and `u_versionToString` follow ICU 78.1 [uinvchar.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/uinvchar.cpp) and [putil.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/putil.cpp), unchanged since 65.1. Firmware 13.42 reports ICU 78.1 and exports none of them ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6400470698)); the [3.20 table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) has all five. Version parts use the guest `strtoul`, truncated to eight bits; `u_versionToString` keeps ICU's dropped zero tens digit (105 becomes `15`). A null buffer with a positive conversion length throws; ICU dereferences it. Closes when: compared with the console. + +### Unknown function info + +- The 150 `sceIcuUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceIcu/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From 458a9978956d223a00e5332c6cd92546bef277bd Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:28:48 +1300 Subject: [PATCH 364/702] docs: distinguish known exports from unverified ABI --- docs/dev/debt/libSceIcu.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index 63a026f939..518c926d21 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -4,6 +4,6 @@ - [Invariant character and version helpers](../../../core/libs/prx/libSceIcu/Version.cpp) are unverified on a console. `u_charsToUChars`, `u_UCharsToChars`, `u_versionFromString`, `u_versionFromUString` and `u_versionToString` follow ICU 78.1 [uinvchar.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/uinvchar.cpp) and [putil.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/putil.cpp), unchanged since 65.1. Firmware 13.42 reports ICU 78.1 and exports none of them ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6400470698)); the [3.20 table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) has all five. Version parts use the guest `strtoul`, truncated to eight bits; `u_versionToString` keeps ICU's dropped zero tens digit (105 becomes `15`). A null buffer with a positive conversion length throws; ICU dereferences it. Closes when: compared with the console. -### Unknown function info +### Known export names; ABI prototypes unverified - The 150 `sceIcuUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceIcu/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From 834739e3fd32d30736bcbd0a99f0568ea3b7ec05 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:45:16 +0100 Subject: [PATCH 365/702] feat(libSceCommonDialog): declare pending exports --- .../prx/libSceCommonDialog/CMakeLists.txt | 2 +- .../libSceCommonDialog/InventoryExports.cpp | 92 +++++++++++++++++++ docs/dev/debt/libSceCommonDialog.md | 19 ++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceCommonDialog/InventoryExports.cpp diff --git a/core/libs/prx/libSceCommonDialog/CMakeLists.txt b/core/libs/prx/libSceCommonDialog/CMakeLists.txt index d770a3c871..d3e8c4b64e 100644 --- a/core/libs/prx/libSceCommonDialog/CMakeLists.txt +++ b/core/libs/prx/libSceCommonDialog/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceCommonDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceCommonDialog SHARED EXCLUDE_FROM_ALL Export.cpp InventoryExports.cpp) target_include_directories(libSceCommonDialog PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceCommonDialog) diff --git a/core/libs/prx/libSceCommonDialog/InventoryExports.cpp b/core/libs/prx/libSceCommonDialog/InventoryExports.cpp new file mode 100644 index 0000000000..72c21afb5d --- /dev/null +++ b/core/libs/prx/libSceCommonDialog/InventoryExports.cpp @@ -0,0 +1,92 @@ +#include <cstddef> +#include <cstdint> +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCommonDialogPending_001_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce16CommonDialogUtil12getSelfAppIdEv", sceCommonDialogPending_001_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_002_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce16CommonDialogUtil6Client11closeModuleEv", sceCommonDialogPending_002_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_003_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce16CommonDialogUtil6Client11updateStateEv", sceCommonDialogPending_003_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_004_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce16CommonDialogUtil6Client15launchCmnDialogEv", sceCommonDialogPending_004_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_005_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce16CommonDialogUtil6ClientD0Ev", sceCommonDialogPending_005_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_006_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce16CommonDialogUtil6ClientD1Ev", sceCommonDialogPending_006_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_007_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZN3sce16CommonDialogUtil6ClientD2Ev", sceCommonDialogPending_007_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_008_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client10isCloseReqEv", sceCommonDialogPending_008_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_009_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client13getFinishDataEPvm", sceCommonDialogPending_009_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_010_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client14getClientStateEv", sceCommonDialogPending_010_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_011_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client19isInitializedStatusEv", sceCommonDialogPending_011_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_012_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client8getAppIdEv", sceCommonDialogPending_012_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_013_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client8isFinishEv", sceCommonDialogPending_013_nid_no_patch); + +int APS5_VABI sceCommonDialogPending_014_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client9getResultEv", sceCommonDialogPending_014_nid_no_patch); + +} diff --git a/docs/dev/debt/libSceCommonDialog.md b/docs/dev/debt/libSceCommonDialog.md index f46aaf0394..9737f50f99 100644 --- a/docs/dev/debt/libSceCommonDialog.md +++ b/docs/dev/debt/libSceCommonDialog.md @@ -4,6 +4,25 @@ - [libSceCommonDialog](../../../core/libs/prx/libSceCommonDialog/Export.cpp) +### Unknown function info + +- 14 additional pending imports in [InventoryExports.cpp](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) are listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCommonDialog.c). Their signatures and behavior are unverified; each uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`, for import resolution only. + +- `_ZN3sce16CommonDialogUtil12getSelfAppIdEv` — NID `2RdicdHhtGA` — [`sceCommonDialogPending_001_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZN3sce16CommonDialogUtil6Client11closeModuleEv` — NID `I+tdxsCap08` — [`sceCommonDialogPending_002_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZN3sce16CommonDialogUtil6Client11updateStateEv` — NID `v4+gzuTkv6k` — [`sceCommonDialogPending_003_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZN3sce16CommonDialogUtil6Client15launchCmnDialogEv` — NID `CwCzG0nnLg8` — [`sceCommonDialogPending_004_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZN3sce16CommonDialogUtil6ClientD0Ev` — NID `Ib1SMmbr07k` — [`sceCommonDialogPending_005_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZN3sce16CommonDialogUtil6ClientD1Ev` — NID `6TIMpGvsrC4` — [`sceCommonDialogPending_006_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZN3sce16CommonDialogUtil6ClientD2Ev` — NID `+UyKxWAnqIU` — [`sceCommonDialogPending_007_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZNK3sce16CommonDialogUtil6Client10isCloseReqEv` — NID `bUCx72-9f0g` — [`sceCommonDialogPending_008_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZNK3sce16CommonDialogUtil6Client13getFinishDataEPvm` — NID `xZtXq554Lbg` — [`sceCommonDialogPending_009_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZNK3sce16CommonDialogUtil6Client14getClientStateEv` — NID `C-EZ3PkhibQ` — [`sceCommonDialogPending_010_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZNK3sce16CommonDialogUtil6Client19isInitializedStatusEv` — NID `70niEKUAnZ0` — [`sceCommonDialogPending_011_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZNK3sce16CommonDialogUtil6Client8getAppIdEv` — NID `mdJgdwoM0Mo` — [`sceCommonDialogPending_012_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZNK3sce16CommonDialogUtil6Client8isFinishEv` — NID `87GekE1nowg` — [`sceCommonDialogPending_013_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) +- `_ZNK3sce16CommonDialogUtil6Client9getResultEv` — NID `6ljeTSi+fjs` — [`sceCommonDialogPending_014_nid_no_patch`](../../../core/libs/prx/libSceCommonDialog/InventoryExports.cpp) + ### Functional - [sceCommonDialogInitialize](../../../core/libs/prx/libSceCommonDialog/Export.cpp) (libSceCommonDialog) - returning `ALREADY_SYSTEM_INITIALIZED` (`0x80B80002`) from a repeated call is not measured on a console by the project. A third-party PS5 log ([obSCEne](https://github.com/project-oops/obSCEne/blob/67b4c529eb3c271184117e26bb54330531ef2cf6/reports/hardware/report-20261014-004800.log#L260-L261), firmware 12.40) records two calls, `0x0` and then `0x80b80002`; a third call is not covered. The SDK error names, shadPS4 (a PS4 emulator) and SharpProspero give the same value; PPSA09695 compares the result with `0x80b80002` (call site 0x5F7BD0) and PPSA06327 stops on `0x80b80004`. Closes when: measured on a PS5, including a third call. From 90ac7f31000cea2268cabf4c9ee0127494914ab9 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:23:33 +0100 Subject: [PATCH 366/702] feat(libSceNpCommon): declare 150 additional PS5 exports --- core/libs/prx/libSceNpCommon/CMakeLists.txt | 2 +- .../libSceNpCommon/PendingExportsRound7.cpp | 758 ++++++++++++++++++ docs/dev/debt/libSceNpCommon.md | 152 +++- 3 files changed, 910 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp diff --git a/core/libs/prx/libSceNpCommon/CMakeLists.txt b/core/libs/prx/libSceNpCommon/CMakeLists.txt index 79edd3a3b4..d4cc13ef65 100644 --- a/core/libs/prx/libSceNpCommon/CMakeLists.txt +++ b/core/libs/prx/libSceNpCommon/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceNpCommon SHARED EXCLUDE_FROM_ALL PendingExports.cpp) +add_library(libSceNpCommon SHARED EXCLUDE_FROM_ALL PendingExports.cpp PendingExportsRound7.cpp) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp") target_sources(libSceNpCommon PRIVATE Export.cpp) endif() diff --git a/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp b/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp new file mode 100644 index 0000000000..162d3e7a07 --- /dev/null +++ b/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp @@ -0,0 +1,758 @@ +#include <cstddef> +#include <cstdint> +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI _ZN3sce2np10CancelableC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilXmlEscape(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np4CondD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceNpAllocatorExRealloc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np8WorkItem6CancelEi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9WorkQueue11GetItemByIdEi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np14JsonDocBuilderC2ERKNS0_9JsonValueE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np18MemoryStreamReaderD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7RingBuf4dtorEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZThn16_N3sce2np9HttpTransD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10MemoryFileD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10EventQueueC2EP16SceNpAllocatorEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np4Cond4ctorEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np16StreamReadBufferD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2np9EventFlag6IsInitEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10CancelLockC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np13NpTitleSecretC2ERK16SceNpTitleSecret(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np8WorkItem10SetRunningEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient21CreateRequestForAsyncEPiiPKvm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np18HttpConnectionPool13InvalidateAllEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient21DeleteRequestForAsyncEii(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np18MemoryStreamWriter5WriteEPNS0_6HandleEPKvmPm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np15CancelableScopeD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np8JsonFileD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9JsonArray12AddItemArrayEPPS1_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6ObjectdaEPv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np14CalloutContextD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9Semaphore4WaitEj(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2npeqERKNS0_4TimeES3_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np13RingBufMemory6ExpandEm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZThn8_N3sce2np10MemoryFile4ReadEPNS0_6HandleEPvmPm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7RingBuf15CheckoutForReadEPm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9EventFlag4OpenEPKc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np13NpTitleSecret5ClearEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10EventQueue7EnqueueEPKvmj(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np12HttpTemplateD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9WorkQueueD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZThn16_N3sce2np9HttpTrans5WriteEPNS0_6HandleEPKvmPm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10EventQueue4ctorEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np12StreamReader4ReadEPNS0_6HandleEPNS0_9StreamCtxEPvmPm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9NpTitleId5ParseEPS1_PKcm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np20BufferedStreamReaderD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc10IpmiClient7ConnectEPKvm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np4Cond4InitEPKcPNS0_5MutexE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient10DisconnectEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc10IpmiClient11IsConnectedEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np12HttpTemplate19SetAuthInfoCallbackEPFii15SceHttpAuthTypePKcPcS5_iPPhPmPiPvESA_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilParseJid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6ObjectnaEmR16SceNpAllocatorEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9Semaphore6CreateEiiPKc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2npleERKNS0_4TimeERK10SceRtcTick(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilXmlUnescape(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpXmlParse(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9WorkQueue15GetFinishedItemENS0_14WorkItemStatusE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9EventFlag4PollEmjPm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9EventFlagD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9LocalFile6RemoveEPKc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np14JsonDocBuilder12BuildBufSizeEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10MemoryFile8TruncateEl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9HttpTrans24GetResponseContentLengthEPNS0_6HandleEPbPm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient13CreateRequestEPiiPKvm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np8SelectorD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10CancelableD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7HttpUriD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np15CancelableScopeD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10Cancelable7DestroyEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZThn8_N3sce2np9LocalFileD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2np10JsonString6GetStrEPcm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np8NpCommIdC2ERK20SceNpCommunicationId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10Cancelable10LockCancelEPKciS3_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np8WorkItem10SetPendingEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7Callout4StopEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7RingBuf14CheckinForReadEm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np12StreamWriter9WriteDataEPNS0_6HandleEPKvmPm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6ThreadD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2npeqERK12SceNpTitleIdRKNS0_9NpTitleIdE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9EventFlag3SetEm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9RefObjectC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2np7RingBuf6IsFullEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2npneERKNS0_13NpTitleSecretERK16SceNpTitleSecret(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9LocalFileD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np16StreamReadBufferD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient11TermServiceEi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6HandleD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient17UnregisterServiceEi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9WorkQueue4ctorEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6Thread4InitEPKcimm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2np4Time18ConvertToPosixTimeEPl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7Callout10IsTimedoutEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7Callout11CalloutFuncEPv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZThn8_N3sce2np6HandleD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6Handle4InitEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZThn16_N3sce2np10MemoryFileD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceNpAllocatorExFree(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient7ConnectEPKvm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7RingBufC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10MemoryFileD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9HttpTrans21GetResponseStatusCodeEPNS0_6HandleEPi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np12StreamReader8SkipDataEPNS0_6HandleElPl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np13NpTitleSecretC2ERKS1_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np14CalloutContextD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np5Mutex7DestroyEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np14JsonDocBuilderC1ERKNS0_9JsonValueE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6ObjectdlEPvR16SceNpAllocatorEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9HttpTransD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np12HttpTemplateC2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6ObjectdaEPvR14SceNpAllocator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9EventFlag6CreateEPKcj(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient15RegisterServiceEPKNS1_17ServiceClientInfoE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np8WorkItemD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9SemaphoreD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np18MemoryStreamReaderC1EPKvm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9EventFlagD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9RefObjectD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceNpAllocatorMalloc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np18MemoryStreamWriterD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2npneERK10SceRtcTickRKNS0_4TimeE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np13RingBufMemory6IsInitEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7RingBufD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6ObjectnwEmR16SceNpAllocatorEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceNpHeapMalloc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np12HttpTemplate7DestroyEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np15CancelableScope5BeginEPNS0_6HandleEPKciS5_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2np10EventQueue6IsInitEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np18MemoryStreamWriterD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np8JsonBool5ClearEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9LocalFile8TruncateEl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2np10JsonNumber6GetNumEPcm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np13RingBufMemory7DestroyEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9RefObject7ReleaseEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9LocalFileC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np7RingBufD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np4PathD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np12StreamReader7ReadAllEPNS0_6HandleEPvmPm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np4CondD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9EventFlag7DestroyEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9HttpTransC1EP16SceNpAllocatorEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np6Thread4dtorEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np4Time8AddHoursEl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9NpTitleIdC2ERK12SceNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np12WorkerThreadD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np13RingBufMemoryD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np9LocalFile5CloseEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2np10JsonString9GetLengthEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np10EventQueueD0Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np8JsonNull5ClearEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np18MemoryStreamWriterD2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np4Time12GetUserClockEPS1_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient12AbortRequestEii(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2np5Mutex6IsInitEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index be7ac29c21..268e974bf4 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,7 +2,7 @@ ### Unknown function info -The 34 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +Pending import declarations are matched to names in the local libSceNpCommon.sprx export table and the public [PS5 3.20 libSceNpCommon export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c); their NIDs are recomputed with tools/nid_names.py. Prototypes and behavior are unknown. Each parameterless declaration exists for import resolution and throws if called. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -56,3 +56,153 @@ The 34 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpOpenSema and sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) forward to the libkernel open and close calls for named semaphores, which still throw. - [sceNpSetPlatformType, sceNpIntGetPlatformType, sceNpIntSetPlatformType, sceNpUtilCanonicalizeNpIdForPs4, sceNpUtilCanonicalizeNpIdForPsp2, sceNpUtilJidToNpId and sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) depend on console platform tags, network domain names or a version string of the console; they throw. - [sceNpJsonParse, sceNpJsonParseInit, sceNpJsonParseBuf, sceNpJsonParseBufInit, sceNpJsonParseEx, sceNpJsonParseExInit and the sceNpServerErrorJson exports](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) are a streaming, schema-checked JSON parser whose event and error rules are not yet specified; they throw. +- [_ZN3sce2np10CancelableC2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `vqekW3s-eFg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilXmlEscape](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `5bBPLZV49kY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np4CondD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `AKiHGWhC2KU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_sceNpAllocatorExRealloc](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `kKF3w-XkCWA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np8WorkItem6CancelEi](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Oq5aepLkEWg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9WorkQueue11GetItemByIdEi](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `F2umEBpQFHc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np14JsonDocBuilderC2ERKNS0_9JsonValueE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `dhJGQPKLmn0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np18MemoryStreamReaderD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `gA0CaCjJpg0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7RingBuf4dtorEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `rvz8xYxhMW0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZThn16_N3sce2np9HttpTransD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `zWIFe+d77PU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10MemoryFileD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `6Vst7HqJMXU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10EventQueueC2EP16SceNpAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `esiO4He2WTU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np4Cond4ctorEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `n9pzAHeCCVU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np16StreamReadBufferD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ZufKqNXItD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZNK3sce2np9EventFlag6IsInitEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `st-oQvV7HVI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10CancelLockC2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `MecB8wAHCfE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np13NpTitleSecretC2ERK16SceNpTitleSecret](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `9QN7g5mQgCU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np8WorkItem10SetRunningEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `XUCjhejJvPc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc17ServiceIpmiClient21CreateRequestForAsyncEPiiPKvm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `v5Z2LAKua28`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np18HttpConnectionPool13InvalidateAllEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `et05S+nkWG8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc17ServiceIpmiClient21DeleteRequestForAsyncEii](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `7oJpAd+vJQA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np18MemoryStreamWriter5WriteEPNS0_6HandleEPKvmPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `VxKQGrudnzk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np15CancelableScopeD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `km5-rjNjSFk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np8JsonFileD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `QFYVZvAJNC8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9JsonArray12AddItemArrayEPPS1_](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `4cIJxNKQK5g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6ObjectdaEPv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `1x0jThSUr4w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np14CalloutContextD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `WR4mjQeqz6s`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9Semaphore4WaitEj](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `hQLw6eE4O44`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2npeqERKNS0_4TimeES3_](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `UVLmT9lzRYA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np13RingBufMemory6ExpandEm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `do0t--lEKMM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZThn8_N3sce2np10MemoryFile4ReadEPNS0_6HandleEPvmPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `E6GYo9uzjds`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7RingBuf15CheckoutForReadEPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `d8NGGmSEFfU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9EventFlag4OpenEPKc](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `5Wy+JxpCBxg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np13NpTitleSecret5ClearEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `X0vEo7cZamA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10EventQueue7EnqueueEPKvmj](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `AV5jHo8O3+E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np12HttpTemplateD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Bm74HLvoNY4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9WorkQueueD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `AIDhc3KCK7w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZThn16_N3sce2np9HttpTrans5WriteEPNS0_6HandleEPKvmPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `eRlqlofFKYg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10EventQueue4ctorEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `xu9qWN0YYC4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np12StreamReader4ReadEPNS0_6HandleEPNS0_9StreamCtxEPvmPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `rCRh3V03bPs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9NpTitleId5ParseEPS1_PKcm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `-swgMjedLUQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np20BufferedStreamReaderD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Tgr66MThOxA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc10IpmiClient7ConnectEPKvm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `CGKtxL26XqI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np4Cond4InitEPKcPNS0_5MutexE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `wWTqVcTnep8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc17ServiceIpmiClient10DisconnectEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `uvYTUK5xYG8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc10IpmiClient11IsConnectedEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `prj9aMR74bA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np12HttpTemplate19SetAuthInfoCallbackEPFii15SceHttpAuthTypePKcPcS5_iPPhPmPiPvESA_](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ezJnmv7hkAg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilParseJid](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `oz2SlXNAnuI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6ObjectnaEmR16SceNpAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `bKMVqRcCQ1U`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9Semaphore6CreateEiiPKc](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `wcOCedFKan4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2npleERKNS0_4TimeERK10SceRtcTick](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `cAeFZE72SXU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilXmlUnescape](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `+0rj9KhmYb0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpXmlParse](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `fEcrs9UPPyo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9WorkQueue15GetFinishedItemENS0_14WorkItemStatusE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `wM4q1JMisvA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9EventFlag4PollEmjPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `37Rd2JS+FCM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9EventFlagD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `uHOOEbuzjEQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9LocalFile6RemoveEPKc](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `haDbtVOmaao`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np14JsonDocBuilder12BuildBufSizeEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `PYBehFWVd60`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10MemoryFile8TruncateEl](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `1DtavqenQjg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9HttpTrans24GetResponseContentLengthEPNS0_6HandleEPbPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `YW09CP0Vrtw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc17ServiceIpmiClient13CreateRequestEPiiPKvm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `H35UsHYlhB4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np8SelectorD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `PA9VYFAVKIE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10CancelableD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `upzdrzOYkS0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7HttpUriD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `wSCKvDDBPy4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np15CancelableScopeD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `E8yuDNYbzl0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10Cancelable7DestroyEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `v2yJZLY0w1U`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZThn8_N3sce2np9LocalFileD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `kD3l0P19Wzg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZNK3sce2np10JsonString6GetStrEPcm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ZquDaA7NrQ4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np8NpCommIdC2ERK20SceNpCommunicationId](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `WywlusFissg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10Cancelable10LockCancelEPKciS3_](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `fd+grYAEph0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np8WorkItem10SetPendingEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `2YbS+GhInZQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7Callout4StopEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `14PDhhMEBKY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7RingBuf14CheckinForReadEm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `oaSKGgwTWG0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np12StreamWriter9WriteDataEPNS0_6HandleEPKvmPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `vHaV+tsSVu4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6ThreadD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `0Q5aKjYErBA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2npeqERK12SceNpTitleIdRKNS0_9NpTitleIdE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `6jHOZ6fItFU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9EventFlag3SetEm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `8i-vOVRVt5w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9RefObjectC1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `brUrttJp6MM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZNK3sce2np7RingBuf6IsFullEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `bvJ207-8qBQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2npneERKNS0_13NpTitleSecretERK16SceNpTitleSecret](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `7b5y1XSa+KQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9LocalFileD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `-n0CR0QxhnY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np16StreamReadBufferD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `bH7ljyLOsBw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc17ServiceIpmiClient11TermServiceEi](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `0rqwC4+sgzU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6HandleD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `YWt5S4-cg9c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc17ServiceIpmiClient17UnregisterServiceEi](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `k9jCtANC+QM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9WorkQueue4ctorEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `XMIv42L5bEA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6Thread4InitEPKcimm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `EqX45DhWUpo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZNK3sce2np4Time18ConvertToPosixTimeEPl](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `qmtHTmA-CcQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7Callout10IsTimedoutEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `xxOTJpEyoj4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7Callout11CalloutFuncEPv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Zw3QlKu49eM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZThn8_N3sce2np6HandleD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `lUsG1QfgVN4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6Handle4InitEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ifqJb-V1QZw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZThn16_N3sce2np10MemoryFileD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `8S1mWU-N9kM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_sceNpAllocatorExFree](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Zh23aSLeeZo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc17ServiceIpmiClient7ConnectEPKvm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `NTPZ5GZIA6U`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7RingBufC1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `L5BnZpuQImk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10MemoryFileD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ZUf92uPkRuA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9HttpTrans21GetResponseStatusCodeEPNS0_6HandleEPi](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `WoFp77mNyw0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np12StreamReader8SkipDataEPNS0_6HandleElPl](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `MSMPXUL5AuM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np13NpTitleSecretC2ERKS1_](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `JHG9CTmkdQw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np14CalloutContextD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `S+a+rgnGX8A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np5Mutex7DestroyEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `RgGW4f0ox1g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np14JsonDocBuilderC1ERKNS0_9JsonValueE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ikFI73f3hP4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6ObjectdlEPvR16SceNpAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `iwDNdnEGyhI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9HttpTransD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `GSVe-aaTiEg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np12HttpTemplateC2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `hw-UPUK9T+w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6ObjectdaEPvR14SceNpAllocator](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `4il4PZAZOnQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9EventFlag6CreateEPKcj](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `amFi-Av19hU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc17ServiceIpmiClient15RegisterServiceEPKNS1_17ServiceClientInfoE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `stUzNgtFmtY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np8WorkItemD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Y-I66cSNp+A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9SemaphoreD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `OukNoRur97E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np18MemoryStreamReaderC1EPKvm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `UHj0GDTA2CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9EventFlagD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `RpWWfCEs9xA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9RefObjectD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `lPQzOhwPjuw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_sceNpAllocatorMalloc](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `mzlILsFx0cU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np18MemoryStreamWriterD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `TXJnPiKuTf8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2npneERK10SceRtcTickRKNS0_4TimeE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `MVY+jtY-WiQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np13RingBufMemory6IsInitEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `zdRXyt-65kA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7RingBufD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `hfJ1gGLgvq8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6ObjectnwEmR16SceNpAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `orRb69nSo64`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_sceNpHeapMalloc](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `kwW5qddf+Lo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np12HttpTemplate7DestroyEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `73qbxKjBH0o`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np15CancelableScope5BeginEPNS0_6HandleEPKciS5_](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Etvu03IpTEc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZNK3sce2np10EventQueue6IsInitEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `VI8AHrfLdqY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np18MemoryStreamWriterD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `3VdCUl+DkNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np8JsonBool5ClearEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `uDyILPgHF9Q`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9LocalFile8TruncateEl](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Sgo7wy9okFI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZNK3sce2np10JsonNumber6GetNumEPcm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `m+dAaZ5pyO4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np13RingBufMemory7DestroyEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Za00SEoNA2A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9RefObject7ReleaseEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `5tYi1l9CXD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9LocalFileC1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `QWlZu1JZOww`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np7RingBufD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `9+NmoosRoBA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np4PathD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `1nF0eXrBZYM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np12StreamReader7ReadAllEPNS0_6HandleEPvmPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `f1ncwa-JXlA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np4CondD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `yX9ISVXv+0M`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9EventFlag7DestroyEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `QlaBcxSFPZI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9HttpTransC1EP16SceNpAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `sqNxD6H5ZOQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np6Thread4dtorEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `-3gV5N2u-sc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np4Time8AddHoursEl](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Zc+a6k6i7gY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9NpTitleIdC2ERK12SceNpTitleId](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `9n60S+t4Cxs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np12WorkerThreadD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `f5L6ax7EWHk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np13RingBufMemoryD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `70qFzq4z3UI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np9LocalFile5CloseEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `9q+h2q5YprU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZNK3sce2np10JsonString9GetLengthEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Wixm7ST0Syw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np10EventQueueD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `E4uoqSdo8ek`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np8JsonNull5ClearEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `88GKkivBFhI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np18MemoryStreamWriterD2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `YmOVGwSJmzk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np4Time12GetUserClockEPS1_](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `vINvzJOaqws`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZN3sce2np3ipc17ServiceIpmiClient12AbortRequestEii](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `tQOrMf4KtIo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [_ZNK3sce2np5Mutex6IsInitEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Yc+qj4TIEY0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From 5b924dc09ae6c2c72fd2ef4e47475a118b5348c7 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:29:54 +0100 Subject: [PATCH 367/702] feat(libSceXml): declare pending exports --- core/libs/prx/libSceXml/Export.cpp | 750 ++++++++++++++++++++++++++++- docs/dev/debt/libSceXml.md | 5 + 2 files changed, 754 insertions(+), 1 deletion(-) create mode 100644 docs/dev/debt/libSceXml.md diff --git a/core/libs/prx/libSceXml/Export.cpp b/core/libs/prx/libSceXml/Export.cpp index fb34206c21..83c8c68aea 100644 --- a/core/libs/prx/libSceXml/Export.cpp +++ b/core/libs/prx/libSceXml/Export.cpp @@ -5,6 +5,754 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceXmlUnknown_PG6C3_Y2PG4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PG6C3+Y2PG4", sceXmlUnknown_PG6C3_Y2PG4_nid_no_patch); + +int APS5_VABI sceXmlUnknown_fEvjAmW_Ius_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fEvjAmW+Ius", sceXmlUnknown_fEvjAmW_Ius_nid_no_patch); + +int APS5_VABI sceXmlUnknown_bjsL0GMcHMI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bjsL0GMcHMI", sceXmlUnknown_bjsL0GMcHMI_nid_no_patch); + +int APS5_VABI sceXmlUnknown_QuhXPuuafi0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QuhXPuuafi0", sceXmlUnknown_QuhXPuuafi0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_0a42xZWTRqA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0a42xZWTRqA", sceXmlUnknown_0a42xZWTRqA_nid_no_patch); + +int APS5_VABI sceXmlUnknown_R2dhCyaDT48_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("R2dhCyaDT48", sceXmlUnknown_R2dhCyaDT48_nid_no_patch); + +int APS5_VABI sceXmlUnknown_ZJgKN_own3w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZJgKN+own3w", sceXmlUnknown_ZJgKN_own3w_nid_no_patch); + +int APS5_VABI sceXmlUnknown_HzmsiSfYg3Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HzmsiSfYg3Q", sceXmlUnknown_HzmsiSfYg3Q_nid_no_patch); + +int APS5_VABI sceXmlUnknown_OUDdcf1tv80_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OUDdcf1tv80", sceXmlUnknown_OUDdcf1tv80_nid_no_patch); + +int APS5_VABI sceXmlUnknown_6lEc1ExhDbE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6lEc1ExhDbE", sceXmlUnknown_6lEc1ExhDbE_nid_no_patch); + +int APS5_VABI sceXmlUnknown_cXQ1mGsUUso_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cXQ1mGsUUso", sceXmlUnknown_cXQ1mGsUUso_nid_no_patch); + +int APS5_VABI sceXmlUnknown_5nlaOBZYquI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5nlaOBZYquI", sceXmlUnknown_5nlaOBZYquI_nid_no_patch); + +int APS5_VABI sceXmlUnknown_WyGkGjCMbhc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WyGkGjCMbhc", sceXmlUnknown_WyGkGjCMbhc_nid_no_patch); + +int APS5_VABI sceXmlUnknown_kaTn7C8H6iU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("kaTn7C8H6iU", sceXmlUnknown_kaTn7C8H6iU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_UrDhHj0xBLs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UrDhHj0xBLs", sceXmlUnknown_UrDhHj0xBLs_nid_no_patch); + +int APS5_VABI sceXmlUnknown_ZnX_pfQnudw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZnX-pfQnudw", sceXmlUnknown_ZnX_pfQnudw_nid_no_patch); + +int APS5_VABI sceXmlUnknown_XarSmMr1Rf0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XarSmMr1Rf0", sceXmlUnknown_XarSmMr1Rf0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_ezzkCWLzmI0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ezzkCWLzmI0", sceXmlUnknown_ezzkCWLzmI0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_NU1VKlqKDrA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NU1VKlqKDrA", sceXmlUnknown_NU1VKlqKDrA_nid_no_patch); + +int APS5_VABI sceXmlUnknown_BmJSILyuz90_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BmJSILyuz90", sceXmlUnknown_BmJSILyuz90_nid_no_patch); + +int APS5_VABI sceXmlUnknown_Z56irUtiXt0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Z56irUtiXt0", sceXmlUnknown_Z56irUtiXt0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_MYonML9r0sQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("MYonML9r0sQ", sceXmlUnknown_MYonML9r0sQ_nid_no_patch); + +int APS5_VABI sceXmlUnknown_X4NiektK72E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("X4NiektK72E", sceXmlUnknown_X4NiektK72E_nid_no_patch); + +int APS5_VABI sceXmlUnknown_VwkgbfMINsc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("VwkgbfMINsc", sceXmlUnknown_VwkgbfMINsc_nid_no_patch); + +int APS5_VABI sceXmlUnknown_zl8amn17Exk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zl8amn17Exk", sceXmlUnknown_zl8amn17Exk_nid_no_patch); + +int APS5_VABI sceXmlUnknown_ak09Q_mYM0k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ak09Q+mYM0k", sceXmlUnknown_ak09Q_mYM0k_nid_no_patch); + +int APS5_VABI sceXmlUnknown_cGZgcrq_L9A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cGZgcrq-L9A", sceXmlUnknown_cGZgcrq_L9A_nid_no_patch); + +int APS5_VABI sceXmlUnknown_hX8j9yVCL4Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hX8j9yVCL4Y", sceXmlUnknown_hX8j9yVCL4Y_nid_no_patch); + +int APS5_VABI sceXmlUnknown_YDUKJ_yIjaE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("YDUKJ-yIjaE", sceXmlUnknown_YDUKJ_yIjaE_nid_no_patch); + +int APS5_VABI sceXmlUnknown_LkOFUiblGSk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LkOFUiblGSk", sceXmlUnknown_LkOFUiblGSk_nid_no_patch); + +int APS5_VABI sceXmlUnknown_4s_kfKFg_5o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4s-kfKFg+5o", sceXmlUnknown_4s_kfKFg_5o_nid_no_patch); + +int APS5_VABI sceXmlUnknown_N1iB3I8VXis_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("N1iB3I8VXis", sceXmlUnknown_N1iB3I8VXis_nid_no_patch); + +int APS5_VABI sceXmlUnknown_R2XV0ewj3dI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("R2XV0ewj3dI", sceXmlUnknown_R2XV0ewj3dI_nid_no_patch); + +int APS5_VABI sceXmlUnknown_SEgecjvVvtM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SEgecjvVvtM", sceXmlUnknown_SEgecjvVvtM_nid_no_patch); + +int APS5_VABI sceXmlUnknown_ahrF_7fmUY8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ahrF-7fmUY8", sceXmlUnknown_ahrF_7fmUY8_nid_no_patch); + +int APS5_VABI sceXmlUnknown_rG2ESToL3K0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("rG2ESToL3K0", sceXmlUnknown_rG2ESToL3K0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_dqsvARv0Bvc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dqsvARv0Bvc", sceXmlUnknown_dqsvARv0Bvc_nid_no_patch); + +int APS5_VABI sceXmlUnknown_xxLlsTkSZN8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xxLlsTkSZN8", sceXmlUnknown_xxLlsTkSZN8_nid_no_patch); + +int APS5_VABI sceXmlUnknown_61hqQqOvL00_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("61hqQqOvL00", sceXmlUnknown_61hqQqOvL00_nid_no_patch); + +int APS5_VABI sceXmlUnknown_nP8b1EXmb4c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("nP8b1EXmb4c", sceXmlUnknown_nP8b1EXmb4c_nid_no_patch); + +int APS5_VABI sceXmlUnknown_AXsvyvweIck_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AXsvyvweIck", sceXmlUnknown_AXsvyvweIck_nid_no_patch); + +int APS5_VABI sceXmlUnknown_lGzRzz4Zp9o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lGzRzz4Zp9o", sceXmlUnknown_lGzRzz4Zp9o_nid_no_patch); + +int APS5_VABI sceXmlUnknown_IYU31xtHBBw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("IYU31xtHBBw", sceXmlUnknown_IYU31xtHBBw_nid_no_patch); + +int APS5_VABI sceXmlUnknown_cdo_5lVs1Zk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cdo-5lVs1Zk", sceXmlUnknown_cdo_5lVs1Zk_nid_no_patch); + +int APS5_VABI sceXmlUnknown_q1eydak0HVM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("q1eydak0HVM", sceXmlUnknown_q1eydak0HVM_nid_no_patch); + +int APS5_VABI sceXmlUnknown_ksmy_WdnC_8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ksmy+WdnC+8", sceXmlUnknown_ksmy_WdnC_8_nid_no_patch); + +int APS5_VABI sceXmlUnknown_Zm_3BN1raUM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Zm+3BN1raUM", sceXmlUnknown_Zm_3BN1raUM_nid_no_patch); + +int APS5_VABI sceXmlUnknown_6rdgvpVBBGI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6rdgvpVBBGI", sceXmlUnknown_6rdgvpVBBGI_nid_no_patch); + +int APS5_VABI sceXmlUnknown_3SsdQzYQzo4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3SsdQzYQzo4", sceXmlUnknown_3SsdQzYQzo4_nid_no_patch); + +int APS5_VABI sceXmlUnknown_r2rDWToC1MQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("r2rDWToC1MQ", sceXmlUnknown_r2rDWToC1MQ_nid_no_patch); + +int APS5_VABI sceXmlUnknown_3HSPRdTF1fw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3HSPRdTF1fw", sceXmlUnknown_3HSPRdTF1fw_nid_no_patch); + +int APS5_VABI sceXmlUnknown_wtDBw_PQhB8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wtDBw-PQhB8", sceXmlUnknown_wtDBw_PQhB8_nid_no_patch); + +int APS5_VABI sceXmlUnknown_hFqu5w4zIiA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hFqu5w4zIiA", sceXmlUnknown_hFqu5w4zIiA_nid_no_patch); + +int APS5_VABI sceXmlUnknown_aVTInRBzsG0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("aVTInRBzsG0", sceXmlUnknown_aVTInRBzsG0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_BpqEaUzi1YI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BpqEaUzi1YI", sceXmlUnknown_BpqEaUzi1YI_nid_no_patch); + +int APS5_VABI sceXmlUnknown_NHsN2nJAVmM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NHsN2nJAVmM", sceXmlUnknown_NHsN2nJAVmM_nid_no_patch); + +int APS5_VABI sceXmlUnknown__AmYlACOriA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-AmYlACOriA", sceXmlUnknown__AmYlACOriA_nid_no_patch); + +int APS5_VABI sceXmlUnknown_DpQmygHE0Mo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DpQmygHE0Mo", sceXmlUnknown_DpQmygHE0Mo_nid_no_patch); + +int APS5_VABI sceXmlUnknown_U79P_srDil8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("U79P-srDil8", sceXmlUnknown_U79P_srDil8_nid_no_patch); + +int APS5_VABI sceXmlUnknown_IXzpGKFGdHE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("IXzpGKFGdHE", sceXmlUnknown_IXzpGKFGdHE_nid_no_patch); + +int APS5_VABI sceXmlUnknown_MOs8PnGhPHU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("MOs8PnGhPHU", sceXmlUnknown_MOs8PnGhPHU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_EM5NLPqpUlc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EM5NLPqpUlc", sceXmlUnknown_EM5NLPqpUlc_nid_no_patch); + +int APS5_VABI sceXmlUnknown_lELUb_ymzQE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lELUb+ymzQE", sceXmlUnknown_lELUb_ymzQE_nid_no_patch); + +int APS5_VABI sceXmlUnknown_KYan1Hzj36c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KYan1Hzj36c", sceXmlUnknown_KYan1Hzj36c_nid_no_patch); + +int APS5_VABI sceXmlUnknown_cTq2UrFRl_s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cTq2UrFRl-s", sceXmlUnknown_cTq2UrFRl_s_nid_no_patch); + +int APS5_VABI sceXmlUnknown_AVskck6Ei5A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AVskck6Ei5A", sceXmlUnknown_AVskck6Ei5A_nid_no_patch); + +int APS5_VABI sceXmlUnknown_AwO_jxy52uE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AwO-jxy52uE", sceXmlUnknown_AwO_jxy52uE_nid_no_patch); + +int APS5_VABI sceXmlUnknown__b6MeTX_oKs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-b6MeTX-oKs", sceXmlUnknown__b6MeTX_oKs_nid_no_patch); + +int APS5_VABI sceXmlUnknown_gbbdoYygOOY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gbbdoYygOOY", sceXmlUnknown_gbbdoYygOOY_nid_no_patch); + +int APS5_VABI sceXmlUnknown_QfB2HoxkyXw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QfB2HoxkyXw", sceXmlUnknown_QfB2HoxkyXw_nid_no_patch); + +int APS5_VABI sceXmlUnknown_sEdWzeyWzU0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sEdWzeyWzU0", sceXmlUnknown_sEdWzeyWzU0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_dqEF7A1NFSw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dqEF7A1NFSw", sceXmlUnknown_dqEF7A1NFSw_nid_no_patch); + +int APS5_VABI sceXmlUnknown_0Am3sBbSwo0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Am3sBbSwo0", sceXmlUnknown_0Am3sBbSwo0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_DYjXSQpmafc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DYjXSQpmafc", sceXmlUnknown_DYjXSQpmafc_nid_no_patch); + +int APS5_VABI sceXmlUnknown_HyC1lgnGOAU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HyC1lgnGOAU", sceXmlUnknown_HyC1lgnGOAU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_CT2gtjWgCN0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("CT2gtjWgCN0", sceXmlUnknown_CT2gtjWgCN0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_pXRO7hiHbZw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pXRO7hiHbZw", sceXmlUnknown_pXRO7hiHbZw_nid_no_patch); + +int APS5_VABI sceXmlUnknown_hXkNZdCO_qU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hXkNZdCO+qU", sceXmlUnknown_hXkNZdCO_qU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_FNyriqDetMY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("FNyriqDetMY", sceXmlUnknown_FNyriqDetMY_nid_no_patch); + +int APS5_VABI sceXmlUnknown_CWHVjeJ6MZA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("CWHVjeJ6MZA", sceXmlUnknown_CWHVjeJ6MZA_nid_no_patch); + +int APS5_VABI sceXmlUnknown_L3ZTOrvokyQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("L3ZTOrvokyQ", sceXmlUnknown_L3ZTOrvokyQ_nid_no_patch); + +int APS5_VABI sceXmlUnknown_zB_KjKIxT3U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zB+KjKIxT3U", sceXmlUnknown_zB_KjKIxT3U_nid_no_patch); + +int APS5_VABI sceXmlUnknown_goUTenum5Qg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("goUTenum5Qg", sceXmlUnknown_goUTenum5Qg_nid_no_patch); + +int APS5_VABI sceXmlUnknown_bn_QzEH_yGU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bn+QzEH+yGU", sceXmlUnknown_bn_QzEH_yGU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_xQEFvezBtjQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xQEFvezBtjQ", sceXmlUnknown_xQEFvezBtjQ_nid_no_patch); + +int APS5_VABI sceXmlUnknown_lO5WE1I_3_c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lO5WE1I+3-c", sceXmlUnknown_lO5WE1I_3_c_nid_no_patch); + +int APS5_VABI sceXmlUnknown_5_ZoQoEBymk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5-ZoQoEBymk", sceXmlUnknown_5_ZoQoEBymk_nid_no_patch); + +int APS5_VABI sceXmlUnknown_DDYywAer5K0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DDYywAer5K0", sceXmlUnknown_DDYywAer5K0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_yHASI8VDPMo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yHASI8VDPMo", sceXmlUnknown_yHASI8VDPMo_nid_no_patch); + +int APS5_VABI sceXmlUnknown_Cf_2kNJZ_kg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Cf-2kNJZ-kg", sceXmlUnknown_Cf_2kNJZ_kg_nid_no_patch); + +int APS5_VABI sceXmlUnknown_fAWof4n_aJk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fAWof4n+aJk", sceXmlUnknown_fAWof4n_aJk_nid_no_patch); + +int APS5_VABI sceXmlUnknown_6WH7RBBXvNE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6WH7RBBXvNE", sceXmlUnknown_6WH7RBBXvNE_nid_no_patch); + +int APS5_VABI sceXmlUnknown_1KUSL3RcrcU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1KUSL3RcrcU", sceXmlUnknown_1KUSL3RcrcU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_Y6ky5K9t_fo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Y6ky5K9t+fo", sceXmlUnknown_Y6ky5K9t_fo_nid_no_patch); + +int APS5_VABI sceXmlUnknown_hcI_VlFn00Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hcI-VlFn00Y", sceXmlUnknown_hcI_VlFn00Y_nid_no_patch); + +int APS5_VABI sceXmlUnknown_BjtxPGQ2_fQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BjtxPGQ2-fQ", sceXmlUnknown_BjtxPGQ2_fQ_nid_no_patch); + +int APS5_VABI sceXmlUnknown_Jn066JNHRQc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Jn066JNHRQc", sceXmlUnknown_Jn066JNHRQc_nid_no_patch); + +int APS5_VABI sceXmlUnknown_niOakMzo8VQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("niOakMzo8VQ", sceXmlUnknown_niOakMzo8VQ_nid_no_patch); + +int APS5_VABI sceXmlUnknown_a_xgqpSqYX8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("a-xgqpSqYX8", sceXmlUnknown_a_xgqpSqYX8_nid_no_patch); + +int APS5_VABI sceXmlUnknown_iwGQqOKMxec_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("iwGQqOKMxec", sceXmlUnknown_iwGQqOKMxec_nid_no_patch); + +int APS5_VABI sceXmlUnknown_Q1PLuSTqHHU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Q1PLuSTqHHU", sceXmlUnknown_Q1PLuSTqHHU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_ncY4waWIyVU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ncY4waWIyVU", sceXmlUnknown_ncY4waWIyVU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_cR4ywoNCJ0M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cR4ywoNCJ0M", sceXmlUnknown_cR4ywoNCJ0M_nid_no_patch); + +int APS5_VABI sceXmlUnknown_c1T_xl90CI8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("c1T+xl90CI8", sceXmlUnknown_c1T_xl90CI8_nid_no_patch); + +int APS5_VABI sceXmlUnknown_EpIfo9S64Yc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EpIfo9S64Yc", sceXmlUnknown_EpIfo9S64Yc_nid_no_patch); + +int APS5_VABI sceXmlUnknown_E1QbfONrBrI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("E1QbfONrBrI", sceXmlUnknown_E1QbfONrBrI_nid_no_patch); + +int APS5_VABI sceXmlUnknown_gyuVdpJihv0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gyuVdpJihv0", sceXmlUnknown_gyuVdpJihv0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_xOC0RMoxhIM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xOC0RMoxhIM", sceXmlUnknown_xOC0RMoxhIM_nid_no_patch); + +int APS5_VABI sceXmlUnknown_lT_hm4QHVUI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lT+hm4QHVUI", sceXmlUnknown_lT_hm4QHVUI_nid_no_patch); + +int APS5_VABI sceXmlUnknown_2NrQs5Oc5NU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2NrQs5Oc5NU", sceXmlUnknown_2NrQs5Oc5NU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_ztPRp3gnFeo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ztPRp3gnFeo", sceXmlUnknown_ztPRp3gnFeo_nid_no_patch); + +int APS5_VABI sceXmlUnknown_oBPtPb7GaMg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("oBPtPb7GaMg", sceXmlUnknown_oBPtPb7GaMg_nid_no_patch); + +int APS5_VABI sceXmlUnknown_h4EDPDzHiX0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("h4EDPDzHiX0", sceXmlUnknown_h4EDPDzHiX0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_GJucNrTY5kk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("GJucNrTY5kk", sceXmlUnknown_GJucNrTY5kk_nid_no_patch); + +int APS5_VABI sceXmlUnknown_t1foZtjoF2w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("t1foZtjoF2w", sceXmlUnknown_t1foZtjoF2w_nid_no_patch); + +int APS5_VABI sceXmlUnknown_Y_75_LnHTVo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Y+75-LnHTVo", sceXmlUnknown_Y_75_LnHTVo_nid_no_patch); + +int APS5_VABI sceXmlUnknown_sfRXuOQix9A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sfRXuOQix9A", sceXmlUnknown_sfRXuOQix9A_nid_no_patch); + +int APS5_VABI sceXmlUnknown_pCEC6vbRldc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pCEC6vbRldc", sceXmlUnknown_pCEC6vbRldc_nid_no_patch); + +int APS5_VABI sceXmlUnknown_4b_vop3WuR4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4b-vop3WuR4", sceXmlUnknown_4b_vop3WuR4_nid_no_patch); + +int APS5_VABI sceXmlUnknown_bC3O0thcHS8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bC3O0thcHS8", sceXmlUnknown_bC3O0thcHS8_nid_no_patch); + +int APS5_VABI sceXmlUnknown_t2dW1YJCdBE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("t2dW1YJCdBE", sceXmlUnknown_t2dW1YJCdBE_nid_no_patch); + +int APS5_VABI sceXmlUnknown_UmfVPAXbsR0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UmfVPAXbsR0", sceXmlUnknown_UmfVPAXbsR0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_e9TNH6eNEnA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("e9TNH6eNEnA", sceXmlUnknown_e9TNH6eNEnA_nid_no_patch); + +int APS5_VABI sceXmlUnknown_Z9kvulfEIVU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Z9kvulfEIVU", sceXmlUnknown_Z9kvulfEIVU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_B6e7H_CKcPQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("B6e7H-CKcPQ", sceXmlUnknown_B6e7H_CKcPQ_nid_no_patch); + +int APS5_VABI sceXmlUnknown_LOdAMm2YXIs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LOdAMm2YXIs", sceXmlUnknown_LOdAMm2YXIs_nid_no_patch); + +int APS5_VABI sceXmlUnknown_20wueHdL0Nk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("20wueHdL0Nk", sceXmlUnknown_20wueHdL0Nk_nid_no_patch); + +int APS5_VABI sceXmlUnknown_UUydNdNg5Aw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UUydNdNg5Aw", sceXmlUnknown_UUydNdNg5Aw_nid_no_patch); + +int APS5_VABI sceXmlUnknown_vlhKurmO2dM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("vlhKurmO2dM", sceXmlUnknown_vlhKurmO2dM_nid_no_patch); + +int APS5_VABI sceXmlUnknown_jO6F_B5EbzU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jO6F+B5EbzU", sceXmlUnknown_jO6F_B5EbzU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_r5YSgAZsO4o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("r5YSgAZsO4o", sceXmlUnknown_r5YSgAZsO4o_nid_no_patch); + +int APS5_VABI sceXmlUnknown_WLaUXcixYPw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WLaUXcixYPw", sceXmlUnknown_WLaUXcixYPw_nid_no_patch); + +int APS5_VABI sceXmlUnknown_BcCD3V30xsk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BcCD3V30xsk", sceXmlUnknown_BcCD3V30xsk_nid_no_patch); + +int APS5_VABI sceXmlUnknown_FySlXOyk40o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("FySlXOyk40o", sceXmlUnknown_FySlXOyk40o_nid_no_patch); + +int APS5_VABI sceXmlUnknown_sL1oNQHcDN0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sL1oNQHcDN0", sceXmlUnknown_sL1oNQHcDN0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_6HaRzv379ew_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6HaRzv379ew", sceXmlUnknown_6HaRzv379ew_nid_no_patch); + +int APS5_VABI sceXmlUnknown_cH8iF18qplw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cH8iF18qplw", sceXmlUnknown_cH8iF18qplw_nid_no_patch); + +int APS5_VABI sceXmlUnknown_uwiT_sz9G8Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uwiT+sz9G8Y", sceXmlUnknown_uwiT_sz9G8Y_nid_no_patch); + +int APS5_VABI sceXmlUnknown_cVQVNFBCvds_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cVQVNFBCvds", sceXmlUnknown_cVQVNFBCvds_nid_no_patch); + +int APS5_VABI sceXmlUnknown__kAtAhAVcd0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+kAtAhAVcd0", sceXmlUnknown__kAtAhAVcd0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_GTZYWYRctR0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("GTZYWYRctR0", sceXmlUnknown_GTZYWYRctR0_nid_no_patch); + +int APS5_VABI sceXmlUnknown_s9TpQC332NU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("s9TpQC332NU", sceXmlUnknown_s9TpQC332NU_nid_no_patch); + +int APS5_VABI sceXmlUnknown_UQSdfLL7qxs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UQSdfLL7qxs", sceXmlUnknown_UQSdfLL7qxs_nid_no_patch); + +int APS5_VABI sceXmlUnknown_UwgBRss72dY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UwgBRss72dY", sceXmlUnknown_UwgBRss72dY_nid_no_patch); + +int APS5_VABI sceXmlUnknown_1m2y_e811xA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1m2y+e811xA", sceXmlUnknown_1m2y_e811xA_nid_no_patch); + +int APS5_VABI sceXmlUnknown__vqI0G6ytM8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-vqI0G6ytM8", sceXmlUnknown__vqI0G6ytM8_nid_no_patch); + +int APS5_VABI sceXmlUnknown_imqT45eFHD4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("imqT45eFHD4", sceXmlUnknown_imqT45eFHD4_nid_no_patch); + +int APS5_VABI sceXmlUnknown_KdQXdHK6YPg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KdQXdHK6YPg", sceXmlUnknown_KdQXdHK6YPg_nid_no_patch); + +int APS5_VABI sceXmlUnknown_YQ3ZHtUvdAk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("YQ3ZHtUvdAk", sceXmlUnknown_YQ3ZHtUvdAk_nid_no_patch); + +int APS5_VABI sceXmlUnknown_TdntSxjU4sk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("TdntSxjU4sk", sceXmlUnknown_TdntSxjU4sk_nid_no_patch); } diff --git a/docs/dev/debt/libSceXml.md b/docs/dev/debt/libSceXml.md new file mode 100644 index 0000000000..7c68c21caf --- /dev/null +++ b/docs/dev/debt/libSceXml.md @@ -0,0 +1,5 @@ +# libSceXml technical debt + +### Unknown function info + +- The 150 `sceXmlUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceXml/Export.cpp) are present in the PS5 3.20 `libSceXml` export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their NIDs are derived from the exported mangled symbols. Their prototypes and behavior are not established; each declaration uses the existing no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. The declarations do not claim an ABI or behavior. From e78f746c207fa8d66963db901f15665154e3164a Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 19:28:51 +1300 Subject: [PATCH 368/702] fix(libSceIcu): drop the two exports main already has --- core/libs/prx/libSceIcu/Export.cpp | 10 ---------- docs/dev/debt/libSceIcu.md | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/core/libs/prx/libSceIcu/Export.cpp b/core/libs/prx/libSceIcu/Export.cpp index 9aef8049dd..12dab64687 100644 --- a/core/libs/prx/libSceIcu/Export.cpp +++ b/core/libs/prx/libSceIcu/Export.cpp @@ -131,11 +131,6 @@ int APS5_VABI sceIcuUnknown_RlZwfEuRDx8_nid_no_patch() { } APS5_EXPORT("RlZwfEuRDx8", sceIcuUnknown_RlZwfEuRDx8_nid_no_patch); -int APS5_VABI sceIcuUnknown_ZFJmsiUqcBI_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("ZFJmsiUqcBI", sceIcuUnknown_ZFJmsiUqcBI_nid_no_patch); - int APS5_VABI sceIcuUnknown__xsbcqOIJmM_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -186,11 +181,6 @@ int APS5_VABI sceIcuUnknown_0cYEtBUJ5zQ_nid_no_patch() { } APS5_EXPORT("0cYEtBUJ5zQ", sceIcuUnknown_0cYEtBUJ5zQ_nid_no_patch); -int APS5_VABI sceIcuUnknown_nOadKT6rn7U_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("nOadKT6rn7U", sceIcuUnknown_nOadKT6rn7U_nid_no_patch); - int APS5_VABI sceIcuUnknown_1SsridDRBYQ_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index 518c926d21..b623b33546 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -6,4 +6,4 @@ ### Known export names; ABI prototypes unverified -- The 150 `sceIcuUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceIcu/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 148 `sceIcuUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceIcu/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From 11caf62300e3b14d463c06d3fe767ef91e4a89e2 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:50:25 +0100 Subject: [PATCH 369/702] fix(libSceCommonDialog): register pending NIDs --- .../libSceCommonDialog/InventoryExports.cpp | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceCommonDialog/InventoryExports.cpp b/core/libs/prx/libSceCommonDialog/InventoryExports.cpp index 72c21afb5d..d0ad42de0e 100644 --- a/core/libs/prx/libSceCommonDialog/InventoryExports.cpp +++ b/core/libs/prx/libSceCommonDialog/InventoryExports.cpp @@ -9,84 +9,84 @@ int APS5_VABI sceCommonDialogPending_001_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZN3sce16CommonDialogUtil12getSelfAppIdEv", sceCommonDialogPending_001_nid_no_patch); +APS5_EXPORT("2RdicdHhtGA", sceCommonDialogPending_001_nid_no_patch); int APS5_VABI sceCommonDialogPending_002_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZN3sce16CommonDialogUtil6Client11closeModuleEv", sceCommonDialogPending_002_nid_no_patch); +APS5_EXPORT("I+tdxsCap08", sceCommonDialogPending_002_nid_no_patch); int APS5_VABI sceCommonDialogPending_003_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZN3sce16CommonDialogUtil6Client11updateStateEv", sceCommonDialogPending_003_nid_no_patch); +APS5_EXPORT("v4+gzuTkv6k", sceCommonDialogPending_003_nid_no_patch); int APS5_VABI sceCommonDialogPending_004_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZN3sce16CommonDialogUtil6Client15launchCmnDialogEv", sceCommonDialogPending_004_nid_no_patch); +APS5_EXPORT("CwCzG0nnLg8", sceCommonDialogPending_004_nid_no_patch); int APS5_VABI sceCommonDialogPending_005_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZN3sce16CommonDialogUtil6ClientD0Ev", sceCommonDialogPending_005_nid_no_patch); +APS5_EXPORT("Ib1SMmbr07k", sceCommonDialogPending_005_nid_no_patch); int APS5_VABI sceCommonDialogPending_006_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZN3sce16CommonDialogUtil6ClientD1Ev", sceCommonDialogPending_006_nid_no_patch); +APS5_EXPORT("6TIMpGvsrC4", sceCommonDialogPending_006_nid_no_patch); int APS5_VABI sceCommonDialogPending_007_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZN3sce16CommonDialogUtil6ClientD2Ev", sceCommonDialogPending_007_nid_no_patch); +APS5_EXPORT("+UyKxWAnqIU", sceCommonDialogPending_007_nid_no_patch); int APS5_VABI sceCommonDialogPending_008_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client10isCloseReqEv", sceCommonDialogPending_008_nid_no_patch); +APS5_EXPORT("bUCx72-9f0g", sceCommonDialogPending_008_nid_no_patch); int APS5_VABI sceCommonDialogPending_009_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client13getFinishDataEPvm", sceCommonDialogPending_009_nid_no_patch); +APS5_EXPORT("xZtXq554Lbg", sceCommonDialogPending_009_nid_no_patch); int APS5_VABI sceCommonDialogPending_010_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client14getClientStateEv", sceCommonDialogPending_010_nid_no_patch); +APS5_EXPORT("C-EZ3PkhibQ", sceCommonDialogPending_010_nid_no_patch); int APS5_VABI sceCommonDialogPending_011_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client19isInitializedStatusEv", sceCommonDialogPending_011_nid_no_patch); +APS5_EXPORT("70niEKUAnZ0", sceCommonDialogPending_011_nid_no_patch); int APS5_VABI sceCommonDialogPending_012_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client8getAppIdEv", sceCommonDialogPending_012_nid_no_patch); +APS5_EXPORT("mdJgdwoM0Mo", sceCommonDialogPending_012_nid_no_patch); int APS5_VABI sceCommonDialogPending_013_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client8isFinishEv", sceCommonDialogPending_013_nid_no_patch); +APS5_EXPORT("87GekE1nowg", sceCommonDialogPending_013_nid_no_patch); int APS5_VABI sceCommonDialogPending_014_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("_ZNK3sce16CommonDialogUtil6Client9getResultEv", sceCommonDialogPending_014_nid_no_patch); +APS5_EXPORT("6ljeTSi+fjs", sceCommonDialogPending_014_nid_no_patch); } From 9c0445daae5bd9ae54e400bb4e79fa2fcdecd53a Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 19:27:57 +1300 Subject: [PATCH 370/702] fix(libSceNpCommon): declare the salvaged exports in the public API --- .../libSceNpCommon/PendingExportsRound7.cpp | 1 + core/libs/prx/libSceNpCommon/include/Api.hpp | 151 ++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp b/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp index 162d3e7a07..4ae852c424 100644 --- a/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceNpCommon/include/Api.hpp" #include <cstddef> #include <cstdint> #include "SceTypes.hpp" diff --git a/core/libs/prx/libSceNpCommon/include/Api.hpp b/core/libs/prx/libSceNpCommon/include/Api.hpp index d4c373e182..1cae74d9d8 100644 --- a/core/libs/prx/libSceNpCommon/include/Api.hpp +++ b/core/libs/prx/libSceNpCommon/include/Api.hpp @@ -217,6 +217,157 @@ int APS5_VABI sceNpUtilJsonGetOneChar(char* dst, std::size_t dstSize, const char int APS5_VABI sceNpUtilGetNthChar(std::size_t index, const char* text, const char** character); int APS5_VABI sceNpUtilHttpUrlEncode(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* dstLength); +int APS5_VABI _ZN3sce2np10CancelableC2Ev(void); +int APS5_VABI sceNpUtilXmlEscape(void); +int APS5_VABI _ZN3sce2np4CondD0Ev(void); +int APS5_VABI _sceNpAllocatorExRealloc(void); +int APS5_VABI _ZN3sce2np8WorkItem6CancelEi(void); +int APS5_VABI _ZN3sce2np9WorkQueue11GetItemByIdEi(void); +int APS5_VABI _ZN3sce2np14JsonDocBuilderC2ERKNS0_9JsonValueE(void); +int APS5_VABI _ZN3sce2np18MemoryStreamReaderD0Ev(void); +int APS5_VABI _ZN3sce2np7RingBuf4dtorEv(void); +int APS5_VABI _ZThn16_N3sce2np9HttpTransD0Ev(void); +int APS5_VABI _ZN3sce2np10MemoryFileD0Ev(void); +int APS5_VABI _ZN3sce2np10EventQueueC2EP16SceNpAllocatorEx(void); +int APS5_VABI _ZN3sce2np4Cond4ctorEv(void); +int APS5_VABI _ZN3sce2np16StreamReadBufferD1Ev(void); +int APS5_VABI _ZNK3sce2np9EventFlag6IsInitEv(void); +int APS5_VABI _ZN3sce2np10CancelLockC2Ev(void); +int APS5_VABI _ZN3sce2np13NpTitleSecretC2ERK16SceNpTitleSecret(void); +int APS5_VABI _ZN3sce2np8WorkItem10SetRunningEv(void); +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient21CreateRequestForAsyncEPiiPKvm(void); +int APS5_VABI _ZN3sce2np18HttpConnectionPool13InvalidateAllEv(void); +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient21DeleteRequestForAsyncEii(void); +int APS5_VABI _ZN3sce2np18MemoryStreamWriter5WriteEPNS0_6HandleEPKvmPm(void); +int APS5_VABI _ZN3sce2np15CancelableScopeD1Ev(void); +int APS5_VABI _ZN3sce2np8JsonFileD2Ev(void); +int APS5_VABI _ZN3sce2np9JsonArray12AddItemArrayEPPS1_(void); +int APS5_VABI _ZN3sce2np6ObjectdaEPv(void); +int APS5_VABI _ZN3sce2np14CalloutContextD0Ev(void); +int APS5_VABI _ZN3sce2np9Semaphore4WaitEj(void); +int APS5_VABI _ZN3sce2npeqERKNS0_4TimeES3_(void); +int APS5_VABI _ZN3sce2np13RingBufMemory6ExpandEm(void); +int APS5_VABI _ZThn8_N3sce2np10MemoryFile4ReadEPNS0_6HandleEPvmPm(void); +int APS5_VABI _ZN3sce2np7RingBuf15CheckoutForReadEPm(void); +int APS5_VABI _ZN3sce2np9EventFlag4OpenEPKc(void); +int APS5_VABI _ZN3sce2np13NpTitleSecret5ClearEv(void); +int APS5_VABI _ZN3sce2np10EventQueue7EnqueueEPKvmj(void); +int APS5_VABI _ZN3sce2np12HttpTemplateD1Ev(void); +int APS5_VABI _ZN3sce2np9WorkQueueD2Ev(void); +int APS5_VABI _ZThn16_N3sce2np9HttpTrans5WriteEPNS0_6HandleEPKvmPm(void); +int APS5_VABI _ZN3sce2np10EventQueue4ctorEv(void); +int APS5_VABI _ZN3sce2np12StreamReader4ReadEPNS0_6HandleEPNS0_9StreamCtxEPvmPm(void); +int APS5_VABI _ZN3sce2np9NpTitleId5ParseEPS1_PKcm(void); +int APS5_VABI _ZN3sce2np20BufferedStreamReaderD1Ev(void); +int APS5_VABI _ZN3sce2np3ipc10IpmiClient7ConnectEPKvm(void); +int APS5_VABI _ZN3sce2np4Cond4InitEPKcPNS0_5MutexE(void); +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient10DisconnectEv(void); +int APS5_VABI _ZN3sce2np3ipc10IpmiClient11IsConnectedEv(void); +int APS5_VABI _ZN3sce2np12HttpTemplate19SetAuthInfoCallbackEPFii15SceHttpAuthTypePKcPcS5_iPPhPmPiPvESA_(void); +int APS5_VABI sceNpUtilParseJid(void); +int APS5_VABI _ZN3sce2np6ObjectnaEmR16SceNpAllocatorEx(void); +int APS5_VABI _ZN3sce2np9Semaphore6CreateEiiPKc(void); +int APS5_VABI _ZN3sce2npleERKNS0_4TimeERK10SceRtcTick(void); +int APS5_VABI sceNpUtilXmlUnescape(void); +int APS5_VABI sceNpXmlParse(void); +int APS5_VABI _ZN3sce2np9WorkQueue15GetFinishedItemENS0_14WorkItemStatusE(void); +int APS5_VABI _ZN3sce2np9EventFlag4PollEmjPm(void); +int APS5_VABI _ZN3sce2np9EventFlagD0Ev(void); +int APS5_VABI _ZN3sce2np9LocalFile6RemoveEPKc(void); +int APS5_VABI _ZN3sce2np14JsonDocBuilder12BuildBufSizeEv(void); +int APS5_VABI _ZN3sce2np10MemoryFile8TruncateEl(void); +int APS5_VABI _ZN3sce2np9HttpTrans24GetResponseContentLengthEPNS0_6HandleEPbPm(void); +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient13CreateRequestEPiiPKvm(void); +int APS5_VABI _ZN3sce2np8SelectorD2Ev(void); +int APS5_VABI _ZN3sce2np10CancelableD1Ev(void); +int APS5_VABI _ZN3sce2np7HttpUriD1Ev(void); +int APS5_VABI _ZN3sce2np15CancelableScopeD0Ev(void); +int APS5_VABI _ZN3sce2np10Cancelable7DestroyEv(void); +int APS5_VABI _ZThn8_N3sce2np9LocalFileD1Ev(void); +int APS5_VABI _ZNK3sce2np10JsonString6GetStrEPcm(void); +int APS5_VABI _ZN3sce2np8NpCommIdC2ERK20SceNpCommunicationId(void); +int APS5_VABI _ZN3sce2np10Cancelable10LockCancelEPKciS3_(void); +int APS5_VABI _ZN3sce2np8WorkItem10SetPendingEv(void); +int APS5_VABI _ZN3sce2np7Callout4StopEv(void); +int APS5_VABI _ZN3sce2np7RingBuf14CheckinForReadEm(void); +int APS5_VABI _ZN3sce2np12StreamWriter9WriteDataEPNS0_6HandleEPKvmPm(void); +int APS5_VABI _ZN3sce2np6ThreadD1Ev(void); +int APS5_VABI _ZN3sce2npeqERK12SceNpTitleIdRKNS0_9NpTitleIdE(void); +int APS5_VABI _ZN3sce2np9EventFlag3SetEm(void); +int APS5_VABI _ZN3sce2np9RefObjectC1Ev(void); +int APS5_VABI _ZNK3sce2np7RingBuf6IsFullEv(void); +int APS5_VABI _ZN3sce2npneERKNS0_13NpTitleSecretERK16SceNpTitleSecret(void); +int APS5_VABI _ZN3sce2np9LocalFileD0Ev(void); +int APS5_VABI _ZN3sce2np16StreamReadBufferD2Ev(void); +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient11TermServiceEi(void); +int APS5_VABI _ZN3sce2np6HandleD1Ev(void); +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient17UnregisterServiceEi(void); +int APS5_VABI _ZN3sce2np9WorkQueue4ctorEv(void); +int APS5_VABI _ZN3sce2np6Thread4InitEPKcimm(void); +int APS5_VABI _ZNK3sce2np4Time18ConvertToPosixTimeEPl(void); +int APS5_VABI _ZN3sce2np7Callout10IsTimedoutEv(void); +int APS5_VABI _ZN3sce2np7Callout11CalloutFuncEPv(void); +int APS5_VABI _ZThn8_N3sce2np6HandleD1Ev(void); +int APS5_VABI _ZN3sce2np6Handle4InitEv(void); +int APS5_VABI _ZThn16_N3sce2np10MemoryFileD1Ev(void); +int APS5_VABI _sceNpAllocatorExFree(void); +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient7ConnectEPKvm(void); +int APS5_VABI _ZN3sce2np7RingBufC1Ev(void); +int APS5_VABI _ZN3sce2np10MemoryFileD1Ev(void); +int APS5_VABI _ZN3sce2np9HttpTrans21GetResponseStatusCodeEPNS0_6HandleEPi(void); +int APS5_VABI _ZN3sce2np12StreamReader8SkipDataEPNS0_6HandleElPl(void); +int APS5_VABI _ZN3sce2np13NpTitleSecretC2ERKS1_(void); +int APS5_VABI _ZN3sce2np14CalloutContextD1Ev(void); +int APS5_VABI _ZN3sce2np5Mutex7DestroyEv(void); +int APS5_VABI _ZN3sce2np14JsonDocBuilderC1ERKNS0_9JsonValueE(void); +int APS5_VABI _ZN3sce2np6ObjectdlEPvR16SceNpAllocatorEx(void); +int APS5_VABI _ZN3sce2np9HttpTransD2Ev(void); +int APS5_VABI _ZN3sce2np12HttpTemplateC2Ev(void); +int APS5_VABI _ZN3sce2np6ObjectdaEPvR14SceNpAllocator(void); +int APS5_VABI _ZN3sce2np9EventFlag6CreateEPKcj(void); +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient15RegisterServiceEPKNS1_17ServiceClientInfoE(void); +int APS5_VABI _ZN3sce2np8WorkItemD0Ev(void); +int APS5_VABI _ZN3sce2np9SemaphoreD2Ev(void); +int APS5_VABI _ZN3sce2np18MemoryStreamReaderC1EPKvm(void); +int APS5_VABI _ZN3sce2np9EventFlagD2Ev(void); +int APS5_VABI _ZN3sce2np9RefObjectD1Ev(void); +int APS5_VABI _sceNpAllocatorMalloc(void); +int APS5_VABI _ZN3sce2np18MemoryStreamWriterD0Ev(void); +int APS5_VABI _ZN3sce2npneERK10SceRtcTickRKNS0_4TimeE(void); +int APS5_VABI _ZN3sce2np13RingBufMemory6IsInitEv(void); +int APS5_VABI _ZN3sce2np7RingBufD0Ev(void); +int APS5_VABI _ZN3sce2np6ObjectnwEmR16SceNpAllocatorEx(void); +int APS5_VABI _sceNpHeapMalloc(void); +int APS5_VABI _ZN3sce2np12HttpTemplate7DestroyEv(void); +int APS5_VABI _ZN3sce2np15CancelableScope5BeginEPNS0_6HandleEPKciS5_(void); +int APS5_VABI _ZNK3sce2np10EventQueue6IsInitEv(void); +int APS5_VABI _ZN3sce2np18MemoryStreamWriterD1Ev(void); +int APS5_VABI _ZN3sce2np8JsonBool5ClearEv(void); +int APS5_VABI _ZN3sce2np9LocalFile8TruncateEl(void); +int APS5_VABI _ZNK3sce2np10JsonNumber6GetNumEPcm(void); +int APS5_VABI _ZN3sce2np13RingBufMemory7DestroyEv(void); +int APS5_VABI _ZN3sce2np9RefObject7ReleaseEv(void); +int APS5_VABI _ZN3sce2np9LocalFileC1Ev(void); +int APS5_VABI _ZN3sce2np7RingBufD2Ev(void); +int APS5_VABI _ZN3sce2np4PathD2Ev(void); +int APS5_VABI _ZN3sce2np12StreamReader7ReadAllEPNS0_6HandleEPvmPm(void); +int APS5_VABI _ZN3sce2np4CondD1Ev(void); +int APS5_VABI _ZN3sce2np9EventFlag7DestroyEv(void); +int APS5_VABI _ZN3sce2np9HttpTransC1EP16SceNpAllocatorEx(void); +int APS5_VABI _ZN3sce2np6Thread4dtorEv(void); +int APS5_VABI _ZN3sce2np4Time8AddHoursEl(void); +int APS5_VABI _ZN3sce2np9NpTitleIdC2ERK12SceNpTitleId(void); +int APS5_VABI _ZN3sce2np12WorkerThreadD0Ev(void); +int APS5_VABI _ZN3sce2np13RingBufMemoryD0Ev(void); +int APS5_VABI _ZN3sce2np9LocalFile5CloseEv(void); +int APS5_VABI _ZNK3sce2np10JsonString9GetLengthEv(void); +int APS5_VABI _ZN3sce2np10EventQueueD0Ev(void); +int APS5_VABI _ZN3sce2np8JsonNull5ClearEv(void); +int APS5_VABI _ZN3sce2np18MemoryStreamWriterD2Ev(void); +int APS5_VABI _ZN3sce2np4Time12GetUserClockEPS1_(void); +int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient12AbortRequestEii(void); +int APS5_VABI _ZNK3sce2np5Mutex6IsInitEv(void); + [[noreturn]] void APS5_VABI sceNpPanic(void); } From 1e1d2368d059cad31cfbce0654c88c50a6ae3406 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:15:43 +0100 Subject: [PATCH 371/702] docs: distinguish known exports from unverified ABI --- docs/dev/debt/libSceXml.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceXml.md b/docs/dev/debt/libSceXml.md index 7c68c21caf..3288b9624c 100644 --- a/docs/dev/debt/libSceXml.md +++ b/docs/dev/debt/libSceXml.md @@ -1,5 +1,5 @@ # libSceXml technical debt -### Unknown function info +### Known export names; ABI prototypes unverified - The 150 `sceXmlUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceXml/Export.cpp) are present in the PS5 3.20 `libSceXml` export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their NIDs are derived from the exported mangled symbols. Their prototypes and behavior are not established; each declaration uses the existing no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. The declarations do not claim an ABI or behavior. From c2999115ab6eced95130a6152f8c75974b429a07 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:31:32 +0000 Subject: [PATCH 372/702] feat(libSceGnmDriverForNeoMode): declare 3 stubs with shadPS4 signatures They still throw through NotImplemented_nid_no_patch; only the parameter lists change. The only source is shadPS4's PS4 prototypes; nothing is measured on a PS5. --- core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index b09d2a8a87..d3f9dffc0e 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -154,7 +154,7 @@ int APS5_VABI sceGnmDestroyWorkloadStream(void) { return 0; } -int APS5_VABI sceGnmDingDongForWorkload(void) { +int APS5_VABI sceGnmDingDongForWorkload(std::uint32_t vqueueId, std::uint32_t nextOffsetDw, std::uint64_t workloadId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -329,7 +329,7 @@ int APS5_VABI sceGnmDrawInitToDefaultContextState400(std::uint32_t* cmdbuf, std: return WriteContextState(cmdbuf, size, 0x100, GnmNeo::ContextState400); } -int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(void) { +int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(std::uint32_t* cmdbuf, std::uint32_t size) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -552,7 +552,7 @@ int APS5_VABI sceGnmRegisterGnmLiveCallbackConfig(void) { return 0; } -int APS5_VABI sceGnmRegisterOwner(void) { +int APS5_VABI sceGnmRegisterOwner(void* handle, const char* name) { return Failure; } From 1725aef9dc9896a72cbc3529b04afb38d0b35693 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:31:32 +0000 Subject: [PATCH 373/702] docs(libSceGnmDriverForNeoMode): record the single-source shadPS4 signatures of the declared stubs --- docs/dev/debt/libSceGnmDriverForNeoMode.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md index 507eeee80f..7c6fab0d8c 100644 --- a/docs/dev/debt/libSceGnmDriverForNeoMode.md +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceGnmDingDongForWorkload, sceGnmDrawInitToDefaultContextStateInternalCommand, sceGnmRegisterOwner](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - throw through `NotImplemented_nid_no_patch`; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. - [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 34 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional From 4f8c6c7be92b0b0e55768ca6626e2ba46212459c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:07:11 +0000 Subject: [PATCH 374/702] fix(libSceGnmDriverForNeoMode): give the three stubs their signatures in the public header main declares them with empty parameter lists in Api.hpp, which conflict with the definitions. --- core/libs/prx/libSceGnmDriverForNeoMode/include/Api.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/include/Api.hpp b/core/libs/prx/libSceGnmDriverForNeoMode/include/Api.hpp index f9ecf8db4c..eb8f0bfa36 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/include/Api.hpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/include/Api.hpp @@ -11,7 +11,7 @@ int APS5_VABI sceGnmCreateWorkloadStream(std::uint64_t name, std::uint32_t* stre int APS5_VABI sceGnmDebugModuleReset(void); int APS5_VABI sceGnmDebugReset(void); int APS5_VABI sceGnmDestroyWorkloadStream(void); -int APS5_VABI sceGnmDingDongForWorkload(void); +int APS5_VABI sceGnmDingDongForWorkload(std::uint32_t vqueueId, std::uint32_t nextOffsetDw, std::uint64_t workloadId); int APS5_VABI sceGnmDispatchDirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t threadsX, std::uint32_t threadsY, std::uint32_t threadsZ, std::uint32_t flags); int APS5_VABI sceGnmDispatchIndirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t flags); int APS5_VABI sceGnmDispatchIndirectOnMec(std::uint32_t* cmdbuf, std::uint32_t size, std::uint64_t args, std::uint32_t modifier); @@ -30,7 +30,7 @@ int APS5_VABI sceGnmDrawInitDefaultHardwareState200(std::uint32_t* cmdbuf, std:: int APS5_VABI sceGnmDrawInitDefaultHardwareState350(std::uint32_t* cmdbuf, std::uint32_t size); int APS5_VABI sceGnmDrawInitToDefaultContextState(std::uint32_t* cmdbuf, std::uint32_t size); int APS5_VABI sceGnmDrawInitToDefaultContextState400(std::uint32_t* cmdbuf, std::uint32_t size); -int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(void); +int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(std::uint32_t* cmdbuf, std::uint32_t size); int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalSize(void); int APS5_VABI sceGnmDrawOpaqueAuto(void); int APS5_VABI sceGnmDriverInternalRetrieveGnmInterface(void); @@ -78,7 +78,7 @@ int APS5_VABI sceGnmQueryResourceRegistrationUserMemoryRequirements(void); int APS5_VABI sceGnmRaiseUserExceptionEvent(void); int APS5_VABI sceGnmRegisterGdsResource(void); int APS5_VABI sceGnmRegisterGnmLiveCallbackConfig(void); -int APS5_VABI sceGnmRegisterOwner(void); +int APS5_VABI sceGnmRegisterOwner(void* handle, const char* name); int APS5_VABI sceGnmRegisterOwnerForSystem(void); int APS5_VABI sceGnmRegisterResource(void); int APS5_VABI sceGnmRequestFlipAndSubmitDoneForWorkload(void); From 37b863a3bebad5e73e8d1534cbfadfd02f6a8130 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:57:40 +0000 Subject: [PATCH 375/702] test(libSceGnmDriverForNeoMode): check sceGnmRegisterOwner outside the no-argument table The table's Query type takes no arguments, which stopped converting once sceGnmRegisterOwner got its (void*, const char*) prototype. The call is checked on its own against Failure instead. --- core/libs/tests/GuestGnmNeoRetail.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestGnmNeoRetail.cpp b/core/libs/tests/GuestGnmNeoRetail.cpp index 3101ebb9b0..e52722a37f 100644 --- a/core/libs/tests/GuestGnmNeoRetail.cpp +++ b/core/libs/tests/GuestGnmNeoRetail.cpp @@ -34,7 +34,6 @@ int main() { sceGnmGetResourceUserData, sceGnmQueryResourceRegistrationUserMemoryRequirements, sceGnmRegisterGdsResource, - sceGnmRegisterOwner, sceGnmRegisterOwnerForSystem, sceGnmRegisterResource, sceGnmSetResourceRegistrationUserMemory, @@ -54,6 +53,7 @@ int main() { sceGnmSetSpiEnableSqCounters, sceGnmSetSpiEnableSqCountersForUnitInstance, }); + if (sceGnmRegisterOwner(nullptr, nullptr) != Failure) std::abort(); RequireAll(NoInterface, { sceGnmDriverInternalRetrieveGnmInterface, sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger, From 280afbe0435eac547f884d49b6899ea48778cb17 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:57:40 +0000 Subject: [PATCH 376/702] docs(libSceGnmDriverForNeoMode): say that sceGnmRegisterOwner returns Failure instead of throwing --- docs/dev/debt/libSceGnmDriverForNeoMode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md index 7c6fab0d8c..8aa80f9741 100644 --- a/docs/dev/debt/libSceGnmDriverForNeoMode.md +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceGnmDingDongForWorkload, sceGnmDrawInitToDefaultContextStateInternalCommand, sceGnmRegisterOwner](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - throw through `NotImplemented_nid_no_patch`; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. +- [sceGnmDingDongForWorkload, sceGnmDrawInitToDefaultContextStateInternalCommand, sceGnmRegisterOwner](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - the first two throw through `NotImplemented_nid_no_patch` and `sceGnmRegisterOwner` returns `Failure` (0x8EEE00FF) without registering anything; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. - [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 34 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional From a695743b3d4dd94d76244ba9ce3e86f08d404897 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 27 Oct 2026 07:59:34 +0100 Subject: [PATCH 377/702] fix(libc): set ERANGE when sinh overflows The host sinh set errno to ERANGE on overflow and sinhf does, but the msun port returned the overflowed value with errno untouched. The overflow path of sinh_nid_postfix sets ERANGE again; msun itself sets no errno. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Math.cpp | 1 + docs/dev/debt/libc.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 123dc3c370..9c76c67f37 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1057,6 +1057,7 @@ double APS5_VABI sinh_nid_postfix(double x) { } if (ix < 0x40862e42) return h * exp_nid_postfix(std::fabs(x)); if (ix <= 0x408633ce) return h * 2.0 * SinhLdexpExpHalf(std::fabs(x)); + errno = ERANGE; return x * shuge; } double APS5_VABI cosh_nid_postfix(double x) { return std::cosh(x); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 21be2f76ac..4ee2b798de 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -151,7 +151,7 @@ - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows and macOS omit per-interface AF_LINK entries and interfaces without an address; on Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if<index>` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports an empty name without querying the host machine name, as a payload process read on a PS5 (firmware 13.60): 0 with only the terminator for a 256-byte and a 1-byte buffer. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); whether a title process or a console with a configured name reads another name, and the integration with `sysctl` and name resolution, are unverified and not implemented. -- [sinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_sinh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_sinh.c) over this file's msun `Expm1` for |x| < 22, where it gives msun's bits (0 of 2,000,000 random arguments in [-20, 20] differ from a locally built msun on a Windows host). From 22 on it calls this file's `exp`, which is still the host's, so those results can differ from msun in the last bit until `exp` follows msun; the last step before the overflow threshold scales that `exp` as msun's `__ldexp_exp` does. It sets no `errno`, as msun's does not. It was compared with msun built on the host, not with a console: the titles' `libc.prx` and the system `libSceLibcInternal` are unmeasured. +- [sinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_sinh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_sinh.c) over this file's msun `Expm1` for |x| < 22, where it gives msun's bits (0 of 2,000,000 random arguments in [-20, 20] differ from a locally built msun on a Windows host). From 22 on it calls this file's `exp`, which is still the host's, so those results can differ from msun in the last bit until `exp` follows msun; the last step before the overflow threshold scales that `exp` as msun's `__ldexp_exp` does. On overflow it sets `errno` to `ERANGE`, as the host's `sinh` did and `sinhf` does; msun itself sets none. It was compared with msun built on the host, not with a console: the titles' `libc.prx` and the system `libSceLibcInternal` are unmeasured. - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, up to six bytes per sequence, with no surrogate or 0x10FFFF check when encoding. The state holds the value in its first eight bytes and the expected byte count (0x10 for a pending low surrogate, 1 for a stored high one) as 16 bits at 0xA. `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (13.60) the system library's default locale converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; a title's bundled `libc.prx` does too until `setlocale` installs a table ([disassembly](https://github.com/boykopovar/AnyPS5/pull/8855#issuecomment-6335267758)) From a7685595cdedd2d9161fa29846c856a6a93bdc1c Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:00:05 +0100 Subject: [PATCH 378/702] test(libc): check the errno sinh sets on overflow Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestMath.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index b028e508b3..0201802b3e 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -733,6 +733,28 @@ static void CheckSinhLargeArguments() { } } +static void CheckSinhOverflowErrno() { + constexpr int sentinel = 1234; + for (const std::uint64_t sign : {std::uint64_t{0}, std::uint64_t{1} << 63}) { + for (const std::uint64_t input : {0x408633cf00000000ull, 0x7fefffffffffffffull}) { + errno = sentinel; + const std::uint64_t result = std::bit_cast<std::uint64_t>(sinh_nid_postfix(std::bit_cast<double>(input | sign))); + if (result != (0x7ff0000000000000ull | sign) || errno != ERANGE) { + std::fprintf(stderr, "Guest sinh overflow failed for %016llx: got %016llx, errno %d\n", static_cast<unsigned long long>(input | sign), static_cast<unsigned long long>(result), errno); + std::abort(); + } + } + for (const std::uint64_t input : {0x408633ce8fb9e87dull, 0x7ff0000000000000ull, 0x3ff0000000000000ull, 0ull}) { + errno = sentinel; + sinh_nid_postfix(std::bit_cast<double>(input | sign)); + if (errno != sentinel) { + std::fprintf(stderr, "Guest sinh changed errno for %016llx: errno %d\n", static_cast<unsigned long long>(input | sign), errno); + std::abort(); + } + } + } +} + static void CheckRoundingNaNConsoleBits() { constexpr std::uint64_t cases[][2] = { {0x7ff0000000000001ull, 0x7ff8000000000001ull}, {0xfff4000000000123ull, 0xfffc000000000123ull}, {0x7ff8000000000456ull, 0x7ff8000000000456ull}, @@ -2000,6 +2022,7 @@ int main() { CheckTanhConsoleBits(); CheckSinhMsunBits(); CheckSinhLargeArguments(); + CheckSinhOverflowErrno(); CheckRoundingNaNConsoleBits(); CheckNextafterEnvironment(nextafterf_nid_postfix, 0x80000000u, 0x3f800000u, 0x00800000u, 0x7f7fffffu, 0x7f800000u); CheckNextafterEnvironment(nextafter_nid_postfix, std::uint64_t{0x8000000000000000ull}, std::uint64_t{0x3ff0000000000000ull}, std::uint64_t{0x0010000000000000ull}, std::uint64_t{0x7fefffffffffffffull}, std::uint64_t{0x7ff0000000000000ull}); From 6b6ec00fad6bd13171c605081e58fe0b3ff79d7e Mon Sep 17 00:00:00 2001 From: Gruzzberry <angus@hutchisons.org> Date: Sun, 25 Oct 2026 22:00:29 +0000 Subject: [PATCH 379/702] feat(libkernel_sys): implement kldsym argument checks Follow FreeBSD 11.4's sys_kldsym: EFAULT for an unreadable lookup structure, EINVAL for a wrong version or command, EFAULT or ENAMETOOLONG for the symbol name, ENOENT for a negative file id. Any other lookup throws, as no kernel module is modelled. Adds GuestCopyIn_nid_no_patch for the checked structure read. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/General.cpp | 7 ++++ .../prx/libkernel_sys/Export.Discovery.cpp | 5 --- core/libs/prx/libkernel_sys/Export.cpp | 18 ++++++++++ core/libs/prx/libkernel_sys/include/Api.hpp | 2 +- core/libs/tests/GuestKernelSys.cpp | 34 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 7 files changed, 62 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 33499fd85e..9ce5bb8cb3 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -33,6 +33,7 @@ extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& desti extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, std::size_t bytes); +extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, std::size_t bytes); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); extern "C" std::filesystem::path ResolveDiskPath_nid_no_patch(const char* path); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 9c68872826..972d2f2070 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -845,6 +845,13 @@ extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, #endif } +extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + const auto start = reinterpret_cast<std::uintptr_t>(source); + if (start == 0 || bytes > UINTPTR_MAX - start) return false; + return CopyGuestBytes(destination, source, bytes); +} + extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { auto resolved = ResolveLocked(path, false, true); AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 9c3d945fbc..3dc427a474 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -125,11 +125,6 @@ int APS5_VABI kldstat_nid_postfix(void) { return 0; } -int APS5_VABI kldsym_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI kmq_notify_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 6f728cbb70..e685cd8363 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -587,6 +587,24 @@ int APS5_VABI kldunload_nid_postfix(int fileid) { return static_cast<int>(Fail(GuestEperm)); } +int APS5_VABI kldsym_nid_postfix(int fileid, int cmd, void* data) { + struct SymbolLookup { + std::int32_t version; + const char* name; + std::uint64_t value; + std::uint64_t size; + }; + static_assert(sizeof(SymbolLookup) == 32); + constexpr int LookupCommand = 1; + SymbolLookup lookup{}; + if (!GuestCopyIn_nid_no_patch(&lookup, data, sizeof(lookup))) return static_cast<int>(Fail(GuestEfault)); + if (lookup.version != static_cast<std::int32_t>(sizeof(lookup)) || cmd != LookupCommand) return static_cast<int>(Fail(GuestEinval)); + std::string name; + if (const int error = CopyGuestPath_nid_no_patch(lookup.name, name)) return static_cast<int>(Fail(error)); + if (fileid != 0) return static_cast<int>(Fail(GuestEnoent)); + throw std::runtime_error("kldsym: looking up the symbol " + name + " in every kernel module is not supported"); +} + int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { std::string targetCopy, createdCopy; if (const int error = CopyPath(target, targetCopy)) return static_cast<int>(Fail(error)); diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index b6d73831b3..71dfe31bb1 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -114,7 +114,7 @@ int APS5_VABI kldfirstmod_nid_postfix(int fileid); int APS5_VABI kldload_nid_postfix(void); int APS5_VABI kldnext_nid_postfix(int fileid); int APS5_VABI kldstat_nid_postfix(void); -int APS5_VABI kldsym_nid_postfix(void); +int APS5_VABI kldsym_nid_postfix(int fileid, int cmd, void* data); int APS5_VABI kldunload_nid_postfix(int fileid); int APS5_VABI kmq_notify_nid_postfix(void); int APS5_VABI kmq_open_nid_postfix(void); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 40e6b87d13..0907565d32 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -930,6 +930,40 @@ int main() { Require(kldunload_nid_postfix(-1) == -1 && Error() == 1); Require(kldunload_nid_postfix(0) == -1 && Error() == 1); Require(kldunload_nid_postfix(1) == -1 && Error() == 1); + struct SymbolLookup { + std::int32_t version; + const char* name; + std::uint64_t value; + std::uint64_t size; + }; + const std::string longSymbol(1024, 's'); + SymbolLookup lookup{32, "symbol", 7, 9}; + Require(kldsym_nid_postfix(-1, 1, nullptr) == -1 && Error() == 14); + Require(kldsym_nid_postfix(-1, 1, reinterpret_cast<void*>(0x10)) == -1 && Error() == 14); + Require(kldsym_nid_postfix(-1, 1, &lookup) == -1 && Error() == 2); + Require(lookup.value == 7 && lookup.size == 9); + for (const std::int32_t version : {0, 24, 31, 33}) { + lookup = {version, "symbol", 0, 0}; + Require(kldsym_nid_postfix(-1, 1, &lookup) == -1 && Error() == 22); + lookup.name = nullptr; + Require(kldsym_nid_postfix(-1, 1, &lookup) == -1 && Error() == 22); + } + for (const int command : {0, 2, -1}) { + lookup = {32, nullptr, 0, 0}; + Require(kldsym_nid_postfix(-1, command, &lookup) == -1 && Error() == 22); + } + lookup = {32, nullptr, 0, 0}; + Require(kldsym_nid_postfix(-1, 1, &lookup) == -1 && Error() == 14); + lookup.name = reinterpret_cast<const char*>(0x10); + Require(kldsym_nid_postfix(0, 1, &lookup) == -1 && Error() == 14); + lookup.name = longSymbol.c_str(); + Require(kldsym_nid_postfix(-1, 1, &lookup) == -1 && Error() == 63); + lookup.name = ""; + Require(kldsym_nid_postfix(-1, 1, &lookup) == -1 && Error() == 2); + lookup.name = "symbol"; + for (const int missing : {-2147483647 - 1, 1, 2, 3, 100, 2147483647}) { + Require(kldsym_nid_postfix(missing, 1, &lookup) == -1 && Error() == 2); + } const std::string throughFile = (root / "file.txt" / "child").string(); Require(pathconf_nid_postfix(throughFile.c_str(), 4) == -1 && Error() == 20); Require(fchmodat_nid_postfix(-100, throughFile.c_str(), 0600, 0) == -1 && Error() == 20); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 2baf982014..460a659bcd 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -45,3 +45,4 @@ - [scePthreadAttrGetscope, scePthreadAttrSetscope, scePthreadAtfork and scePthreadSetconcurrency](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) call libkernel's `pthread_attr_getscope`, `pthread_attr_setscope`, `pthread_atfork` and `pthread_setconcurrency` and turn a POSIX error into `0x80020000 | errno`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/kernel/threads/pthread_attr.cpp#L392) exports `scePthreadAttrGetscope` as `ORBIS(posix_pthread_attr_getscope)`; shadPS4 has no export for the other three, whose names and NIDs are in the [symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and whose signatures follow the same POSIX forms. Not measured on a console. As in libkernel, the scope is 0 (process) or 2 (system), and `scePthreadAtfork` handlers are only recorded, as no process forks - [AMM kernel write commands](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - signatures of the map, map direct, unmap, remap, multimap and protection writers and their GPU mask id forms 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. Each writer builds its record with the encoder of its `libSceAmpr` command buffer sibling, so it applies the sibling's checks and writes its bytes. The GPU mask id is ignored, as by the siblings, and a protection or mask with bits above 32 is rejected, as by `sceKernelWriteModifyMtypeProtectCommand`. A wider `type` is cut to the record's 32 bits, as by that writer; the console's handling is not measured - [sceKernelWaitCommandBufferCompletion](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - signature taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), where `sceAmprAmmWaitCommandBufferCompletion` forwards to it; like that sibling it waits for an AMM submission and returns `SCE_KERNEL_ERROR_ESRCH` for an id that was never submitted, not confirmed on a title +- [kldsym](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_kldsym`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_linker.c) in the order it checks its arguments: `EFAULT` for an unreadable lookup structure, `EINVAL` for a `version` other than the structure size (32 bytes on amd64) or a command other than `KLDSYM_LOOKUP`, `EFAULT` or `ENAMETOOLONG` for the symbol name, `ENOENT` for a file id that no file has. The linker file list is empty here, as in `kldnext` and `kldfind`, so every file id other than 0 gets `ENOENT`; modules a console preloads are not modelled. A lookup in id 0 (every file) throws, as no symbol table is modelled. Whether a title may query kernel symbols is unverified From 3e1dfe558f8c4240c4a0ef3fc4f640b02a6e8230 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 07:08:04 +0000 Subject: [PATCH 380/702] docs(libSceSigninDialog): note the result size and field three titles use Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceSigninDialog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSigninDialog.md b/docs/dev/debt/libSceSigninDialog.md index 5353db25a6..43d77f69c0 100644 --- a/docs/dev/debt/libSceSigninDialog.md +++ b/docs/dev/debt/libSceSigninDialog.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceSigninDialogClose, sceSigninDialogGetResult](../../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes are the 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware +- [sceSigninDialogClose, sceSigninDialogGetResult](../../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes are the 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware. The callers agree with the 16-byte size and the result at offset 0: before their one `sceSigninDialogGetResult` call, PPSA01494, PPSA30839 and PPSA31162 zero a 16-byte result on the stack, and right after it compare its first 32-bit word with 1 From acd5ee3ff572519075028e56343fba959173d5ec Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 07:09:42 +0000 Subject: [PATCH 381/702] docs(libSceGameLiveStreaming): note the status fields three titles read Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceGameLiveStreaming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index ea70d1d1fc..c0cc7bcb4a 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -4,7 +4,7 @@ - [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - a call outside `sceGameLiveStreamingInitialize` and `sceGameLiveStreamingTerminate` throws, as the module's error for it is not confirmed (`sceGameLiveStreamingTerminate` and `sceGameLiveStreamingGetCurrentStatus2` return `0x80A00004`; [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L18) does not return its `SCE_GAME_LIVE_STREAMING_ERROR_NOT_INITIALIZED` from this call). Otherwise both values are accepted and change nothing, as no broadcast is ever on air. The signature, `int sceGameLiveStreamingEnableLiveStreaming(const bool)`, follows [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L810) and [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L111) - [libSceGameLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - native initialization requires exactly 0x4000 heap bytes before checking repeated initialization. Host lifecycle and pointer errors now match the wrapper, but service registration, allocation failures and asynchronous service readiness are not modelled. The host becomes ready immediately after initialization. -- [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - the supplied module confirms the 72-byte layout and inactive status (user -1, remaining bytes zero). Live service counters, active-user changes and broadcast-resolution conversion are unimplemented. +- [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - the supplied module confirms the 72-byte layout and inactive status (user -1, remaining bytes zero). Live service counters, active-user changes and broadcast-resolution conversion are unimplemented. In the 25 instructions after their one call, PPSA01494, PPSA30839 and PPSA31162 read `isOnAir` (the byte at 4) and `spectatorCounts` (the word at 8), so those two are the fields these titles act on; here they stay false and 0. - [sceGameLiveStreamingGetProgramInfo](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the PS5 13.60 module clears the 0x134C-byte program info, asks the system service for its first 0xFD4 bytes and returns `0x80A00002` for a null pointer and `0x80A00004` before initialization. With nothing broadcast here the info stays zero, which assumes the service reports an empty program when nothing is on air; the field layout is unknown. Two titles (PPSA03001 at 0x1ad86b4, PPSA07426 at 0xe89594) disassembled for this entry pass the address of a stack buffer with no store into it between the start of the block and the call; for a result other than 0 they do not read it, for 0 they copy three text fields at offsets 0, 0x802 and 0xC03, and they do not tell `0x80A00002` from `0x80A00004`. The buffer size is not shown. ### Unknown function info From 190ed6c8fbf5e00a95cbcc408decf8b63ee8f546 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:11:03 +0100 Subject: [PATCH 382/702] feat(libkernel): guest memory protection and VirtualQuery on macOS hosts - mprotect: the guest rounds a range to its 16 KiB pages. A Mach-O image is loaded at a multiple of the host page size only (4 KiB under Rosetta), so the pages are counted from where the ELF address 0 of the image holding the range was loaded, and a rounded edge smaller than a guest page that lies in the gap between two segments, where no host page exists, is left out. The write watch is told the new protection. - sceKernelVirtualQuery reads the region with mach_vm_region. - sceKernelMunlockall unlocks the ranges libkernel locked, one by one: macOS has no munlockall, which fails there with ENOSYS and would overwrite errno. - The stack protect test reads page protections with mach_vm_region. This is mugurc's f66a4df741 from his macOS series, without the mlock check that #10889 adds. Co-authored-by: mugurc <mceker@gmail.com> --- .../prx/libc/include/GuestAllocations.hpp | 3 ++ core/libs/prx/libc/src/GuestAllocations.cpp | 53 +++++++++++++++++++ .../libkernel/DirectMemory/DirectMemory.cpp | 17 ++++-- .../prx/libkernel/DirectMemory/Export.cpp | 17 +++++- core/libs/tests/GuestKernelStackProtect.cpp | 14 +++++ docs/dev/debt/libkernel.md | 1 + 6 files changed, 99 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index c94fd68bd1..987cb64631 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -62,6 +62,8 @@ void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); void GuestAllocationsRequireAvailable_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsCovers_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); +void GuestAllocationsTrimToRegistered_nid_postfix(void* mutation, const void** pointer, std::size_t* bytes); +void GuestAllocationsGuestPages_nid_postfix(std::uintptr_t address, std::size_t bytes, std::size_t page, std::uintptr_t* first, std::uintptr_t* end); bool GuestAllocationsOverlaps_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsForRegistered_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function<void(const void*, std::size_t)>& visit); Range GuestAllocationsFind_nid_postfix(void* mutation, const void* pointer); @@ -101,6 +103,7 @@ public: void RequireUnpinned(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireUnpinned_nid_postfix(handle, pointer, bytes); } void RequireAvailable(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireAvailable_nid_postfix(handle, pointer, bytes); } bool Covers(const void* pointer, std::size_t bytes) const { return GuestAllocationsCovers_nid_postfix(handle, pointer, bytes); } + void TrimToRegistered(const void*& pointer, std::size_t& bytes) const { GuestAllocationsTrimToRegistered_nid_postfix(handle, &pointer, &bytes); } bool Overlaps(const void* pointer, std::size_t bytes) const { return GuestAllocationsOverlaps_nid_postfix(handle, pointer, bytes); } bool ForRegistered(const void* pointer, std::size_t bytes, const std::function<void(const void*, std::size_t)>& visit) const { return GuestAllocationsForRegistered_nid_postfix(handle, pointer, bytes, visit); } Range Find(const void* pointer) const { return GuestAllocationsFind_nid_postfix(handle, pointer); } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index a127ca1b60..100831fa63 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -15,6 +15,11 @@ #define NOMINMAX #endif #include <windows.h> +#elif defined(__APPLE__) +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include <mach-o/dyld.h> +#include <mach-o/loader.h> +#include <unistd.h> #else #include <cerrno> #include <cstring> @@ -524,6 +529,54 @@ bool GuestAllocationsForRegistered_nid_postfix(void*, const void* pointer, std:: return any; } +void GuestAllocationsGuestPages_nid_postfix(std::uintptr_t address, std::size_t bytes, std::size_t page, std::uintptr_t* first, std::uintptr_t* end) { + std::uintptr_t bias = 0; +#ifdef __APPLE__ + struct Search { + std::uintptr_t address; + std::uintptr_t bias; + } search{address, 0}; + dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) { + auto& found = *static_cast<Search*>(data); + for (unsigned index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + const auto start = static_cast<std::uintptr_t>(image->dlpi_addr + header.p_vaddr); + if (header.p_type == PT_LOAD && found.address >= start && found.address - start < header.p_memsz) { + found.bias = static_cast<std::uintptr_t>(image->dlpi_addr); + return 1; + } + } + return 0; + }, &search); + bias = search.bias; +#endif + const auto mask = static_cast<std::uintptr_t>(page - 1); + *first = bias + ((address - bias) & ~mask); + *end = bias + ((address + bytes - bias + mask) & ~mask); +} + +void GuestAllocationsTrimToRegistered_nid_postfix(void*, const void** pointer, std::size_t* bytes) { +#ifdef __APPLE__ + constexpr std::uint64_t guestPage = 0x4000; + auto start = reinterpret_cast<std::uintptr_t>(*pointer); + auto end = start + *bytes; + const auto& ranges = registry().ranges; + const auto following = ranges.upper_bound(start); + const bool startCovered = following != ranges.begin() && std::prev(following)->first + std::prev(following)->second->bytes > start; + if (!startCovered && following != ranges.end() && following->first < end && following->first - start < guestPage) start = following->first; + const auto beyond = ranges.lower_bound(end); + if (beyond != ranges.begin()) { + const auto finish = std::prev(beyond)->first + std::prev(beyond)->second->bytes; + if (finish < end && finish > start && end - finish < guestPage) end = finish; + } + *pointer = reinterpret_cast<const void*>(start); + *bytes = end - start; +#else + static_cast<void>(pointer); + static_cast<void>(bytes); +#endif +} + bool GuestAllocationsCovers_nid_postfix(void*, const void* pointer, std::size_t bytes) { const auto address = reinterpret_cast<std::uintptr_t>(pointer); require(address != 0 && bytes != 0 && bytes <= std::numeric_limits<std::uint64_t>::max() - address, "invalid guest allocation range"); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 544bc31ff9..4306e87b25 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1325,10 +1325,13 @@ int DoMprotect(const void* addr, size_t len, int prot) { constexpr auto pageMask = static_cast<std::uintptr_t>(PS5_PAGE_SIZE - 1); const auto limit = std::numeric_limits<std::uintptr_t>::max(); if (address == 0 || len == 0 || len > limit - address || address + len > limit - pageMask) throw std::invalid_argument("Invalid guest memory protection range"); - const auto first = address & ~pageMask; - const auto end = (address + len + pageMask) & ~pageMask; - const auto bytes = static_cast<std::size_t>(end - first); - const auto* pointer = reinterpret_cast<const void*>(first); + auto first = address & ~pageMask; + auto end = (address + len + pageMask) & ~pageMask; +#ifdef __APPLE__ + GuestAllocations::GuestAllocationsGuestPages_nid_postfix(address, len, PS5_PAGE_SIZE, &first, &end); +#endif + auto bytes = static_cast<std::size_t>(end - first); + const void* pointer = reinterpret_cast<const void*>(first); const auto nativeProtection = LinuxProtFromSce(prot); if (GuestThreadStackHostRangeOverlaps(address, address + len)) throw std::system_error(EACCES, std::generic_category(), "mprotect overlaps host-only guest thread stack pages"); @@ -1395,14 +1398,18 @@ int DoMprotect(const void* addr, size_t len, int prot) { if (!image.guestModule) throw std::invalid_argument("Memory protection of a foreign image is not supported"); mutation.RegisterImage(reinterpret_cast<const void*>(image.base)); } + mutation.TrimToRegistered(pointer, bytes); #endif std::vector<std::pair<const void*, std::size_t>> spans; mutation.ForRegistered(pointer, bytes, [&](const void* span, std::size_t spanBytes) { spans.emplace_back(span, spanBytes); }); - if (spans.empty() || reinterpret_cast<std::uintptr_t>(spans.front().first) != first) return SCE_KERNEL_ERROR_EINVAL; + if (spans.empty() || spans.front().first != pointer) return SCE_KERNEL_ERROR_EINVAL; for (const auto& [span, spanBytes] : spans) mutation.RequireUnpinned(span, spanBytes); for (const auto& [span, spanBytes] : spans) { mutation.Protect(span, spanBytes, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { if (mprotect(const_cast<void*>(span), spanBytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); +#ifdef __APPLE__ + GuestWriteWatch::GuestWriteWatchProtectionChanged_nid_postfix(span, spanBytes, nativeProtection); +#endif }); RecordProtection(span, spanBytes, prot); } diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 1b6b1d3dba..0d0f9f1476 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -389,6 +389,17 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf info->end = info->start + host.RegionSize; const bool writable = (host.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_WRITECOPY)) != 0; const bool executable = (host.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) != 0; + #elif defined(__APPLE__) + mach_vm_address_t region = address; + mach_vm_size_t regionSize = 0; + vm_region_basic_info_data_64_t host{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + if (mach_vm_region(mach_task_self(), ®ion, ®ionSize, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&host), &count, &object) != KERN_SUCCESS || region > address || (host.protection & VM_PROT_READ) == 0) return SCE_KERNEL_ERROR_EACCES; + info->start = region; + info->end = region + regionSize; + const bool writable = (host.protection & VM_PROT_WRITE) != 0; + const bool executable = (host.protection & VM_PROT_EXECUTE) != 0; #else std::ifstream maps("/proc/self/maps"); std::string line; @@ -765,7 +776,7 @@ int APS5_VABI sceKernelMunlock_nid_postfix(void* address, std::uint64_t length) } int APS5_VABI sceKernelMunlockall_nid_postfix() { -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__APPLE__) ::munlockall(); #endif std::map<uintptr_t, size_t> ranges; @@ -791,6 +802,10 @@ int APS5_VABI sceKernelMunlockall_nid_postfix() { cursor = next; } } +#elif defined(__APPLE__) + const int saved = errno; + for (const auto& [start, len] : ranges) ::munlock(reinterpret_cast<void*>(start), len); + errno = saved; #endif return 0; } diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp index 2f06f2fab3..3eced016ef 100644 --- a/core/libs/tests/GuestKernelStackProtect.cpp +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -13,6 +13,10 @@ #define NOMINMAX #endif #include <windows.h> +#elif defined(__APPLE__) +#include <mach/mach.h> +#include <mach/mach_vm.h> +#include <sys/mman.h> #else #include <sys/mman.h> #include <sys/resource.h> @@ -39,6 +43,16 @@ static PageState QueryPage(std::uintptr_t address) { Require(VirtualQuery(reinterpret_cast<const void*>(address), &memory, sizeof(memory)) == sizeof(memory)); const bool mapped = memory.State == MEM_COMMIT; return {mapped, mapped && (memory.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE)) != 0, (static_cast<std::uint64_t>(memory.State) << 32) | memory.Protect}; +#elif defined(__APPLE__) + mach_vm_address_t region = address; + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + if (mach_vm_region(mach_task_self(), ®ion, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &count, &object) != KERN_SUCCESS || region > address) return {false, false, 0}; + const bool readable = (info.protection & VM_PROT_READ) != 0; + const bool writable = (info.protection & VM_PROT_WRITE) != 0; + return {true, readable && writable, 1ull | (readable ? 2ull : 0) | (writable ? 4ull : 0) | ((info.protection & VM_PROT_EXECUTE) != 0 ? 8ull : 0)}; #else std::FILE* maps = std::fopen("/proc/self/maps", "r"); Require(maps != nullptr); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 382be036bd..ade0e81e8e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -259,6 +259,7 @@ - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:<port>` stays `127.0.0.1:<port>`), because a bound socket cannot be bound again, and `getsockname` reports that host address. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), and on a connected datagram socket it disconnects first, as on a PS5 (firmware 13.60) and in FreeBSD 11.4's `soconnect()`. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 (measured for an unbound socket) unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There the call returns the length without the host, as Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range fails with `EBUSY`, as it did in a payload process on a PS5 (firmware 13.60) for anonymous memory, locked or not, also with a hole unmapped inside it; a title process, reservations, direct memory and flexible memory were not measured +- Guest memory protection on macOS ([mprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp), [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): the guest rounds a range to its 16 KiB pages, counted from where the ELF address 0 of the image holding it was loaded, which is a multiple of the host page size only (4 KiB under Rosetta). A rounded edge smaller than a guest page that lies in the gap between two segments of an image, where no host page exists, is left out of the request. `sceKernelVirtualQuery` reports the region `mach_vm_region` returns, so its extent is the host's. After a protection change the write watch is told the protection, which the host reports for a watched page as its own - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it - [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). XPSemu (PPSA97358) imports `mlockall` and `munlockall`; their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. From 149ca06f159c241dd5e3201a376afd84129ade7c Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:07:22 +0100 Subject: [PATCH 383/702] test(libkernel): run the main stack checks of the stack protect test on macOS The main stack checks run the test again in a child through /proc/self/exe, which macOS does not have, so the child exited with 127 and the check failed before testing anything. On macOS the child now runs the path _NSGetExecutablePath reports. --- core/libs/tests/GuestKernelStackProtect.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp index 3eced016ef..de0a98ee71 100644 --- a/core/libs/tests/GuestKernelStackProtect.cpp +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -14,6 +14,8 @@ #endif #include <windows.h> #elif defined(__APPLE__) +#include <climits> +#include <mach-o/dyld.h> #include <mach/mach.h> #include <mach/mach_vm.h> #include <sys/mman.h> @@ -515,7 +517,13 @@ static bool ChildPasses(const char* mode, bool unlimitedStack) { if (child == 0) { rlimit limit{}; if (unlimitedStack && getrlimit(RLIMIT_STACK, &limit) == 0) limit.rlim_cur = RLIM_INFINITY; +#ifdef __APPLE__ + char path[PATH_MAX]; + std::uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) == 0 && (!unlimitedStack || setrlimit(RLIMIT_STACK, &limit) == 0)) execl(path, path, mode, static_cast<char*>(nullptr)); +#else if (!unlimitedStack || setrlimit(RLIMIT_STACK, &limit) == 0) execl("/proc/self/exe", "/proc/self/exe", mode, static_cast<char*>(nullptr)); +#endif std::_Exit(127); } int status = 0; From 16feb3387f33e743171729aa75c2fb2705abb620 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:03:03 +0100 Subject: [PATCH 384/702] test(libkernel): keep the main stack checks of the stack protect test on Linux The previous commit ran the main stack checks on macOS through _NSGetExecutablePath, but on macOS every child skips them: the stack is mapped within 1 MiB of its bottom and its limit cannot be raised. They are now built and run on Linux only, as in mugurc's macOS series, and other hosts run the thread stack check alone. --- core/libs/tests/GuestKernelStackProtect.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp index de0a98ee71..2354076a1d 100644 --- a/core/libs/tests/GuestKernelStackProtect.cpp +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -14,8 +14,6 @@ #endif #include <windows.h> #elif defined(__APPLE__) -#include <climits> -#include <mach-o/dyld.h> #include <mach/mach.h> #include <mach/mach_vm.h> #include <sys/mman.h> @@ -464,7 +462,7 @@ static void ProtectHostStacks() { for (void* partial : {static_cast<void*>(nullptr), reinterpret_cast<void*>(1)}) RunOnReservedStack(ProtectReservedStack, partial); RunOnReservedStack(ProtectStackBottom, nullptr); } -#else +#elif defined(__linux__) static int ProtectUnlimitedStack() { int local = 0; void* start = nullptr; @@ -517,13 +515,7 @@ static bool ChildPasses(const char* mode, bool unlimitedStack) { if (child == 0) { rlimit limit{}; if (unlimitedStack && getrlimit(RLIMIT_STACK, &limit) == 0) limit.rlim_cur = RLIM_INFINITY; -#ifdef __APPLE__ - char path[PATH_MAX]; - std::uint32_t size = sizeof(path); - if (_NSGetExecutablePath(path, &size) == 0 && (!unlimitedStack || setrlimit(RLIMIT_STACK, &limit) == 0)) execl(path, path, mode, static_cast<char*>(nullptr)); -#else if (!unlimitedStack || setrlimit(RLIMIT_STACK, &limit) == 0) execl("/proc/self/exe", "/proc/self/exe", mode, static_cast<char*>(nullptr)); -#endif std::_Exit(127); } int status = 0; @@ -548,12 +540,16 @@ int main(int argc, char** argv) { (void)argc; (void)argv; ProtectHostStacks(); -#else +#elif defined(__linux__) if (argc > 1 && std::strcmp(argv[1], "--unlimited-stack") == 0) return ProtectUnlimitedStack(); if (argc > 1 && std::strcmp(argv[1], "--read-only-bottom") == 0) return ProtectMainStackBottom({1}, 3); if (argc > 1 && std::strcmp(argv[1], "--reprotect-bottom") == 0) return ProtectMainStackBottom({0, 3}, 7); ProtectMainStacks(); ProtectStack(0); +#else + (void)argc; + (void)argv; + ProtectStack(0); #endif std::atomic<bool> passed{false}; Pthread thread = nullptr; From f2e8d5a3ebc7d0bf2aab88585cb825649095d729 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:03:03 +0100 Subject: [PATCH 385/702] fix(libkernel): address the macOS memory protection review - mlock on macOS wires only the pages libkernel has not wired yet. XNU counts each wire of a page, and the locked ranges merge overlapping locks, so a page locked twice stayed wired after munlock and munlockall. The mlock test checks the wire count on macOS. - mprotect changes a watched page through GuestWriteWatchProtect, which applies the host protection and records it under the write watch's lock, so a collection on another thread cannot run between the two. The write watch test covers it. - sceKernelVirtualQuery takes a watched page's write permission from the write watch for memory outside the guest allocation registry. - GuestAllocationsGuestPages counts an image segment up to the end of its last host page, as the image page collection does. - The macOS includes stay in main's include block, so the host code other pull requests add there still sees them; the unused Mach-O headers and the end trim, which never changed the protected spans, are gone, and the guest page size is passed in. - The libc and libkernel debt entries describe what is wired now. --- .../prx/libc/include/GuestAllocations.hpp | 4 +- .../libs/prx/libc/include/GuestWriteWatch.hpp | 1 + core/libs/prx/libc/src/GuestAllocations.cpp | 27 ++++---- core/libs/prx/libc/src/GuestWriteWatch.cpp | 27 ++++++++ .../libkernel/DirectMemory/DirectMemory.cpp | 8 ++- .../prx/libkernel/DirectMemory/Export.cpp | 63 ++++++++++++++----- core/libs/tests/GuestMlock.cpp | 26 ++++++++ core/libs/tests/WriteWatch.cpp | 12 ++++ docs/dev/debt/libc.md | 2 +- docs/dev/debt/libkernel.md | 2 +- 10 files changed, 134 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 987cb64631..32472fedf3 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -62,7 +62,7 @@ void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); void GuestAllocationsRequireAvailable_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsCovers_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); -void GuestAllocationsTrimToRegistered_nid_postfix(void* mutation, const void** pointer, std::size_t* bytes); +void GuestAllocationsTrimToRegistered_nid_postfix(void* mutation, std::size_t page, const void** pointer, std::size_t* bytes); void GuestAllocationsGuestPages_nid_postfix(std::uintptr_t address, std::size_t bytes, std::size_t page, std::uintptr_t* first, std::uintptr_t* end); bool GuestAllocationsOverlaps_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsForRegistered_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function<void(const void*, std::size_t)>& visit); @@ -103,7 +103,7 @@ public: void RequireUnpinned(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireUnpinned_nid_postfix(handle, pointer, bytes); } void RequireAvailable(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireAvailable_nid_postfix(handle, pointer, bytes); } bool Covers(const void* pointer, std::size_t bytes) const { return GuestAllocationsCovers_nid_postfix(handle, pointer, bytes); } - void TrimToRegistered(const void*& pointer, std::size_t& bytes) const { GuestAllocationsTrimToRegistered_nid_postfix(handle, &pointer, &bytes); } + void TrimToRegistered(std::size_t page, const void*& pointer, std::size_t& bytes) const { GuestAllocationsTrimToRegistered_nid_postfix(handle, page, &pointer, &bytes); } bool Overlaps(const void* pointer, std::size_t bytes) const { return GuestAllocationsOverlaps_nid_postfix(handle, pointer, bytes); } bool ForRegistered(const void* pointer, std::size_t bytes, const std::function<void(const void*, std::size_t)>& visit) const { return GuestAllocationsForRegistered_nid_postfix(handle, pointer, bytes, visit); } Range Find(const void* pointer) const { return GuestAllocationsFind_nid_postfix(handle, pointer); } diff --git a/core/libs/prx/libc/include/GuestWriteWatch.hpp b/core/libs/prx/libc/include/GuestWriteWatch.hpp index 9afcf582db..96f09551c6 100644 --- a/core/libs/prx/libc/include/GuestWriteWatch.hpp +++ b/core/libs/prx/libc/include/GuestWriteWatch.hpp @@ -17,6 +17,7 @@ bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t byte void GuestWriteWatchBeginHostWrite_nid_postfix(const void* pointer, std::size_t bytes); void GuestWriteWatchEndHostWrite_nid_postfix(const void* pointer, std::size_t bytes); void GuestWriteWatchProtectionChanged_nid_postfix(const void* pointer, std::size_t bytes, int protection); +int GuestWriteWatchProtect_nid_postfix(void* pointer, std::size_t bytes, int protection); int GuestWriteWatchProtection_nid_postfix(std::uintptr_t address, std::uintptr_t limit, std::uintptr_t* runEnd); bool GuestWriteWatchCollectArmed_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context); bool GuestWriteWatchCollectFresh_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context); diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 100831fa63..58d240f166 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -15,11 +15,6 @@ #define NOMINMAX #endif #include <windows.h> -#elif defined(__APPLE__) -#include "prx/libc/include/specifics/linux/ElfTypes.hpp" -#include <mach-o/dyld.h> -#include <mach-o/loader.h> -#include <unistd.h> #else #include <cerrno> #include <cstring> @@ -37,6 +32,9 @@ #include <sys/mman.h> #include <unistd.h> #endif +#ifdef __APPLE__ +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#endif namespace GuestAllocations { namespace { @@ -534,14 +532,16 @@ void GuestAllocationsGuestPages_nid_postfix(std::uintptr_t address, std::size_t #ifdef __APPLE__ struct Search { std::uintptr_t address; + std::uintptr_t hostPage; std::uintptr_t bias; - } search{address, 0}; + } search{address, static_cast<std::uintptr_t>(::sysconf(_SC_PAGESIZE)), 0}; dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) { auto& found = *static_cast<Search*>(data); for (unsigned index = 0; index < image->dlpi_phnum; ++index) { const auto& header = image->dlpi_phdr[index]; const auto start = static_cast<std::uintptr_t>(image->dlpi_addr + header.p_vaddr); - if (header.p_type == PT_LOAD && found.address >= start && found.address - start < header.p_memsz) { + const auto finish = (start + header.p_memsz + found.hostPage - 1) & ~(found.hostPage - 1); + if (header.p_type == PT_LOAD && found.address >= start && found.address < finish) { found.bias = static_cast<std::uintptr_t>(image->dlpi_addr); return 1; } @@ -555,23 +555,18 @@ void GuestAllocationsGuestPages_nid_postfix(std::uintptr_t address, std::size_t *end = bias + ((address + bytes - bias + mask) & ~mask); } -void GuestAllocationsTrimToRegistered_nid_postfix(void*, const void** pointer, std::size_t* bytes) { +void GuestAllocationsTrimToRegistered_nid_postfix(void*, std::size_t page, const void** pointer, std::size_t* bytes) { #ifdef __APPLE__ - constexpr std::uint64_t guestPage = 0x4000; auto start = reinterpret_cast<std::uintptr_t>(*pointer); - auto end = start + *bytes; + const auto end = start + *bytes; const auto& ranges = registry().ranges; const auto following = ranges.upper_bound(start); const bool startCovered = following != ranges.begin() && std::prev(following)->first + std::prev(following)->second->bytes > start; - if (!startCovered && following != ranges.end() && following->first < end && following->first - start < guestPage) start = following->first; - const auto beyond = ranges.lower_bound(end); - if (beyond != ranges.begin()) { - const auto finish = std::prev(beyond)->first + std::prev(beyond)->second->bytes; - if (finish < end && finish > start && end - finish < guestPage) end = finish; - } + if (!startCovered && following != ranges.end() && following->first < end && following->first - start < page) start = following->first; *pointer = reinterpret_cast<const void*>(start); *bytes = end - start; #else + static_cast<void>(page); static_cast<void>(pointer); static_cast<void>(bytes); #endif diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index e0a4344e67..0d83d52ccb 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/GuestWriteWatch.hpp" #include <algorithm> #include <array> +#include <cerrno> #include <cstdio> #include <cstdlib> #include <iterator> @@ -433,6 +434,20 @@ public: }); } + int Protect(std::uintptr_t begin, std::uintptr_t end, int protection) { + if (!_available) return ::mprotect(reinterpret_cast<void*>(begin), end - begin, protection) == 0 ? 0 : errno; + const Lock lock(_lock); + if (::mprotect(reinterpret_cast<void*>(begin), end - begin, protection) != 0) return errno; + const auto first = pageStart(begin); + const auto last = pageEnd(end); + if (first < last) { + forEachPage(first, last, [&](std::uintptr_t, std::uint8_t& state) { + state = static_cast<std::uint8_t>(Written | (state & Fresh) | (protection & ProtectionMask)); + }); + } + return 0; + } + void BeginHostWrite(std::uintptr_t begin, std::uintptr_t end) { if (!_available || end <= begin) return; begin = pageStart(begin); @@ -749,6 +764,18 @@ void GuestWriteWatchProtectionChanged_nid_postfix(const void* pointer, std::size #endif } +int GuestWriteWatchProtect_nid_postfix(void* pointer, std::size_t bytes, int protection) { +#if defined(__APPLE__) + const auto begin = reinterpret_cast<std::uintptr_t>(pointer); + return Watch::Get().Protect(begin, begin + bytes, protection); +#else + static_cast<void>(pointer); + static_cast<void>(bytes); + static_cast<void>(protection); + return ENOSYS; +#endif +} + int GuestWriteWatchProtection_nid_postfix(std::uintptr_t address, std::uintptr_t limit, std::uintptr_t* runEnd) { #if defined(__APPLE__) return Watch::Get().Protection(address, limit, runEnd); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 4306e87b25..d107524192 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1398,7 +1398,7 @@ int DoMprotect(const void* addr, size_t len, int prot) { if (!image.guestModule) throw std::invalid_argument("Memory protection of a foreign image is not supported"); mutation.RegisterImage(reinterpret_cast<const void*>(image.base)); } - mutation.TrimToRegistered(pointer, bytes); + mutation.TrimToRegistered(PS5_PAGE_SIZE, pointer, bytes); #endif std::vector<std::pair<const void*, std::size_t>> spans; mutation.ForRegistered(pointer, bytes, [&](const void* span, std::size_t spanBytes) { spans.emplace_back(span, spanBytes); }); @@ -1406,9 +1406,11 @@ int DoMprotect(const void* addr, size_t len, int prot) { for (const auto& [span, spanBytes] : spans) mutation.RequireUnpinned(span, spanBytes); for (const auto& [span, spanBytes] : spans) { mutation.Protect(span, spanBytes, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { - if (mprotect(const_cast<void*>(span), spanBytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); #ifdef __APPLE__ - GuestWriteWatch::GuestWriteWatchProtectionChanged_nid_postfix(span, spanBytes, nativeProtection); + if (const int error = GuestWriteWatch::GuestWriteWatchProtect_nid_postfix(const_cast<void*>(span), spanBytes, nativeProtection); error != 0) + throw std::system_error(error, std::generic_category(), "mprotect failed"); +#else + if (mprotect(const_cast<void*>(span), spanBytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); #endif }); RecordProtection(span, spanBytes, prot); diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 0d0f9f1476..5bc56db7c3 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -6,6 +6,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" @@ -36,6 +37,8 @@ #include <map> #include <cstdio> #include <mutex> +#include <utility> +#include <vector> extern "C" std::uint64_t GuestImageSegmentBytes_nid_no_patch(); @@ -172,8 +175,7 @@ void _releaseFlexible(uintptr_t start, size_t len) { std::mutex g_lockedRangesLock; std::map<uintptr_t, size_t> g_lockedRanges; -void RecordLockedRange(uintptr_t start, uintptr_t end) { - std::lock_guard lock(g_lockedRangesLock); +void RecordLockedRangeLocked(uintptr_t start, uintptr_t end) { auto it = g_lockedRanges.begin(); while (it != g_lockedRanges.end()) { if (it->first <= end && it->first + it->second >= start) { @@ -187,6 +189,11 @@ void RecordLockedRange(uintptr_t start, uintptr_t end) { g_lockedRanges[start] = end - start; } +void RecordLockedRange(uintptr_t start, uintptr_t end) { + std::lock_guard lock(g_lockedRangesLock); + RecordLockedRangeLocked(start, end); +} + void ForgetLockedRange(uintptr_t start, uintptr_t end) { std::lock_guard lock(g_lockedRangesLock); auto it = g_lockedRanges.upper_bound(start); @@ -398,7 +405,9 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf if (mach_vm_region(mach_task_self(), ®ion, ®ionSize, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&host), &count, &object) != KERN_SUCCESS || region > address || (host.protection & VM_PROT_READ) == 0) return SCE_KERNEL_ERROR_EACCES; info->start = region; info->end = region + regionSize; - const bool writable = (host.protection & VM_PROT_WRITE) != 0; + std::uintptr_t watchedEnd = 0; + const int watched = GuestWriteWatch::GuestWriteWatchProtection_nid_postfix(address, address + 1, &watchedEnd); + const bool writable = watched >= 0 ? (watched & PROT_WRITE) != 0 : (host.protection & VM_PROT_WRITE) != 0; const bool executable = (host.protection & VM_PROT_EXECUTE) != 0; #else std::ifstream maps("/proc/self/maps"); @@ -702,9 +711,39 @@ int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { return 0; } #else +#ifdef __APPLE__ +int WireHostPages(std::uintptr_t start, std::uintptr_t end) { + std::lock_guard lock(g_lockedRangesLock); + std::vector<std::pair<std::uintptr_t, std::uintptr_t>> unwired; + auto cursor = start; + auto it = g_lockedRanges.upper_bound(start); + if (it != g_lockedRanges.begin()) --it; + for (; it != g_lockedRanges.end() && it->first < end; ++it) { + const auto lockedEnd = it->first + it->second; + if (lockedEnd <= cursor) continue; + if (it->first > cursor) unwired.emplace_back(cursor, it->first); + cursor = std::max(cursor, lockedEnd); + } + if (cursor < end) unwired.emplace_back(cursor, end); + for (std::size_t index = 0; index < unwired.size(); ++index) { + if (::mlock(reinterpret_cast<void*>(unwired[index].first), unwired[index].second - unwired[index].first) != 0) { + const int error = errno; + for (std::size_t wired = 0; wired < index; ++wired) ::munlock(reinterpret_cast<void*>(unwired[wired].first), unwired[wired].second - unwired[wired].first); + return error; + } + } + RecordLockedRangeLocked(start, end); + return 0; +} +#else +int WireHostPages(std::uintptr_t start, std::uintptr_t end) { + if (::mlock(reinterpret_cast<void*>(start), end - start) != 0) return errno; + RecordLockedRange(start, end); + return 0; +} +#endif + int LockHostPages(std::uintptr_t start, std::uintptr_t end) { - auto* const address = reinterpret_cast<void*>(start); - const std::size_t bytes = end - start; #ifdef __APPLE__ for (mach_vm_address_t cursor = start; cursor < end;) { mach_vm_address_t region = cursor; @@ -719,20 +758,14 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { cursor = region + size; } #endif - if (::mlock(address, bytes) == 0) { - RecordLockedRange(start, end); - return 0; - } - int error = errno; + int error = WireHostPages(start, end); + if (error == 0) return 0; rlimit limit{}; if ((error == ENOMEM || error == EPERM || error == EAGAIN) && getrlimit(RLIMIT_MEMLOCK, &limit) == 0 && limit.rlim_cur < limit.rlim_max) { limit.rlim_cur = limit.rlim_max; if (setrlimit(RLIMIT_MEMLOCK, &limit) == 0) { - if (::mlock(address, bytes) == 0) { - RecordLockedRange(start, end); - return 0; - } - error = errno; + error = WireHostPages(start, end); + if (error == 0) return 0; } } switch (error) { diff --git a/core/libs/tests/GuestMlock.cpp b/core/libs/tests/GuestMlock.cpp index 43b0f46d48..946afc19e2 100644 --- a/core/libs/tests/GuestMlock.cpp +++ b/core/libs/tests/GuestMlock.cpp @@ -16,6 +16,10 @@ #include <fstream> #include <string> #endif +#ifdef __APPLE__ +#include <mach/mach.h> +#include <mach/mach_vm.h> +#endif static void Require(bool value) { if (!value) std::abort(); } @@ -47,6 +51,19 @@ static std::size_t LockedKilobytes() { } #endif +#ifdef __APPLE__ +static unsigned WiredCount(const void* page) { + mach_vm_address_t address = reinterpret_cast<mach_vm_address_t>(page); + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + Require(mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &count, &object) == KERN_SUCCESS); + Require(address <= reinterpret_cast<mach_vm_address_t>(page)); + return info.user_wired_count; +} +#endif + int main() { constexpr int untouched = 12345; constexpr std::size_t page = 0x4000; @@ -71,6 +88,9 @@ int main() { #endif Require(mlock_nid_postfix(mapped, length) == 0); Require(mlock_nid_postfix(mapped, length) == 0); +#ifdef __APPLE__ + Require(WiredCount(mapped) == 1); +#endif bytes[length - 1] = 7; *__error_nid_postfix() = untouched; @@ -85,6 +105,9 @@ int main() { *__error_nid_postfix() = untouched; Require(munlock_nid_postfix(mapped, length) == 0); Require(*__error_nid_postfix() == untouched); +#ifdef __APPLE__ + Require(WiredCount(mapped) == 0); +#endif bytes[0] = 7; Require(mlock_nid_postfix(mapped, page) == 0); #ifdef HAS_LOCKED_KILOBYTES @@ -107,11 +130,14 @@ int main() { #ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() - lockedBefore == page / 1024); #endif + Require(mlock_nid_postfix(mapped, page) == 0); *__error_nid_postfix() = untouched; Require(munlockall_nid_postfix() == 0); Require(*__error_nid_postfix() == untouched); #ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() == lockedBefore); +#elif defined(__APPLE__) + Require(WiredCount(mapped) == 0); #elif defined(_WIN32) Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); #endif diff --git a/core/libs/tests/WriteWatch.cpp b/core/libs/tests/WriteWatch.cpp index 0588e469d0..0949643a2f 100644 --- a/core/libs/tests/WriteWatch.cpp +++ b/core/libs/tests/WriteWatch.cpp @@ -229,6 +229,18 @@ void CheckProtection() { region.base[2 * region.page] = 1; region.base[4 * region.page] = 1; Require(!GuestWriteWatchCovers_nid_postfix(region.Address(2), region.page), "an unregistered range is covered"); + + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + static_cast<void>(region.Collect()); + Require(GuestWriteWatchProtect_nid_postfix(region.base, region.page, PROT_READ) == 0, "cannot protect a watched page"); + Require(KernelProtection(region.base) == PROT_READ, "a protect does not change the host protection"); + Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(6), &runEnd) == PROT_READ && runEnd == region.Address(1), "a protect is not recorded"); + Require(region.Collect() == std::set<std::size_t>({0}), "a protect is not reported as a write"); + Require(GuestWriteWatchProtect_nid_postfix(region.base, region.page, PROT_READ | PROT_WRITE) == 0, "cannot restore a watched page"); + static_cast<void>(region.Collect()); + region.base[0] = 1; + Require(region.Collect() == std::set<std::size_t>({0}), "a write after a protect is not reported"); + Require(GuestWriteWatchProtect_nid_postfix(region.base + 1, region.page, PROT_READ) == EINVAL, "a misaligned protect does not fail with EINVAL"); } void CheckOverlaps() { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 788c06d8ba..aecaed82cc 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -136,7 +136,7 @@ - [Exception runtime on macOS](../../../core/libs/prx/libc/src/exception/Exports.cpp) (libc) - on macOS an exception that a library throws with a plain `throw` and that nothing catches skips the guest's `std::set_terminate` handler and the guest runtime's report: it ends in libc++abi's terminate, because the host keeps libc++abi there, while on Linux and Windows libc's runtime takes these throws. The `SIGALRM` expiry of `alarm` and `setitimer` throws through the guest runtime on macOS; `NotImplemented_nid_no_patch` cannot, since host code catches its `std::runtime_error`, so an unimplemented call inside a `noexcept` export (`mmap` on an `app0.zar` file) bypasses the guest handler - 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 (file reads and socket receives), as [Host writes into guest memory](../CONVENTIONS.md#host-writes-into-guest-memory) requires; 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; + - code that changes the protection of watched pages has to do it through `GuestWriteWatchProtect`, which changes the host protection and the recorded one under the watch's lock, as `mprotect` in libkernel does; code that maps over them has to call `GuestWriteWatchProtectionChanged`. 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, as `sceKernelVirtualQuery` does for memory outside the guest allocation registry; - 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. - On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [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` diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index ade0e81e8e..2446220e3e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -259,7 +259,7 @@ - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:<port>` stays `127.0.0.1:<port>`), because a bound socket cannot be bound again, and `getsockname` reports that host address. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), and on a connected datagram socket it disconnects first, as on a PS5 (firmware 13.60) and in FreeBSD 11.4's `soconnect()`. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 (measured for an unbound socket) unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There the call returns the length without the host, as Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range fails with `EBUSY`, as it did in a payload process on a PS5 (firmware 13.60) for anonymous memory, locked or not, also with a hole unmapped inside it; a title process, reservations, direct memory and flexible memory were not measured -- Guest memory protection on macOS ([mprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp), [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): the guest rounds a range to its 16 KiB pages, counted from where the ELF address 0 of the image holding it was loaded, which is a multiple of the host page size only (4 KiB under Rosetta). A rounded edge smaller than a guest page that lies in the gap between two segments of an image, where no host page exists, is left out of the request. `sceKernelVirtualQuery` reports the region `mach_vm_region` returns, so its extent is the host's. After a protection change the write watch is told the protection, which the host reports for a watched page as its own +- Guest memory protection on macOS ([mprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp), [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): the guest rounds a range to its 16 KiB pages, counted from where the ELF address 0 of the image holding it was loaded, which is a multiple of the host page size only (4 KiB under Rosetta). A rounded edge smaller than a guest page at the start of the request that lies in the gap between two segments of an image, where no host page exists, is left out. For memory outside the guest allocation registry `sceKernelVirtualQuery` reports the region `mach_vm_region` returns, so its extent is the host's, and takes a watched page's write permission from the write watch - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it - [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). XPSemu (PPSA97358) imports `mlockall` and `munlockall`; their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. From f7f88a4886b63b6f2e3cb340d43e0953d2ef50f8 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:32:24 +0100 Subject: [PATCH 386/702] fix(libkernel): wire a remapped locked range again on macOS - mlock on macOS skipped every page the locked ranges recorded, but a mapping that replaces a locked range in place leaves the record behind without wires, so a later mlock returned 0 and wired nothing. Within a recorded range it now wires again the host regions whose wire count is 0. The mlock test locks, remaps the range fixed and locks again. - munlock and munlockall on macOS hold the locked-range lock across the host unwire and the record update, so a concurrent mlock cannot see a range as wired while it is being unwired. - sceKernelVirtualQuery limits the host region it reports to the write watch run that holds the address, so the permissions it reports hold for the whole extent. - The libc debt entry says what the mapping paths do: they register the range again, and GuestWriteWatchProtectionChanged has no caller yet. --- .../prx/libkernel/DirectMemory/Export.cpp | 58 ++++++++++++++++--- core/libs/tests/GuestMlock.cpp | 5 ++ docs/dev/debt/libc.md | 2 +- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 5bc56db7c3..cffd7e88da 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -194,8 +194,7 @@ void RecordLockedRange(uintptr_t start, uintptr_t end) { RecordLockedRangeLocked(start, end); } -void ForgetLockedRange(uintptr_t start, uintptr_t end) { - std::lock_guard lock(g_lockedRangesLock); +void ForgetLockedRangeLocked(uintptr_t start, uintptr_t end) { auto it = g_lockedRanges.upper_bound(start); if (it != g_lockedRanges.begin()) --it; while (it != g_lockedRanges.end() && it->first < end) { @@ -208,6 +207,11 @@ void ForgetLockedRange(uintptr_t start, uintptr_t end) { } } +void ForgetLockedRange(uintptr_t start, uintptr_t end) { + std::lock_guard lock(g_lockedRangesLock); + ForgetLockedRangeLocked(start, end); +} + } void ReleaseFlexibleMappingRange(std::uintptr_t address, size_t len) { @@ -405,8 +409,18 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf if (mach_vm_region(mach_task_self(), ®ion, ®ionSize, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&host), &count, &object) != KERN_SUCCESS || region > address || (host.protection & VM_PROT_READ) == 0) return SCE_KERNEL_ERROR_EACCES; info->start = region; info->end = region + regionSize; - std::uintptr_t watchedEnd = 0; - const int watched = GuestWriteWatch::GuestWriteWatchProtection_nid_postfix(address, address + 1, &watchedEnd); + int watched = -1; + for (std::uintptr_t cursor = info->start; cursor < info->end;) { + std::uintptr_t runEnd = info->end; + const int protection = GuestWriteWatch::GuestWriteWatchProtection_nid_postfix(cursor, info->end, &runEnd); + if (address < runEnd) { + info->start = cursor; + info->end = runEnd; + watched = protection; + break; + } + cursor = runEnd; + } const bool writable = watched >= 0 ? (watched & PROT_WRITE) != 0 : (host.protection & VM_PROT_WRITE) != 0; const bool executable = (host.protection & VM_PROT_EXECUTE) != 0; #else @@ -712,6 +726,24 @@ int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { } #else #ifdef __APPLE__ +void AddUnwiredHostPages(std::uintptr_t start, std::uintptr_t end, std::vector<std::pair<std::uintptr_t, std::uintptr_t>>& unwired) { + for (auto cursor = start; cursor < end;) { + mach_vm_address_t region = cursor; + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + if (mach_vm_region(mach_task_self(), ®ion, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &count, &object) != KERN_SUCCESS || region >= end) { + unwired.emplace_back(cursor, end); + return; + } + if (region > cursor) unwired.emplace_back(cursor, region); + const auto last = std::min<std::uintptr_t>(end, region + size); + if (info.user_wired_count == 0) unwired.emplace_back(std::max<std::uintptr_t>(cursor, region), last); + cursor = last; + } +} + int WireHostPages(std::uintptr_t start, std::uintptr_t end) { std::lock_guard lock(g_lockedRangesLock); std::vector<std::pair<std::uintptr_t, std::uintptr_t>> unwired; @@ -722,6 +754,7 @@ int WireHostPages(std::uintptr_t start, std::uintptr_t end) { const auto lockedEnd = it->first + it->second; if (lockedEnd <= cursor) continue; if (it->first > cursor) unwired.emplace_back(cursor, it->first); + AddUnwiredHostPages(std::max(cursor, it->first), std::min(end, lockedEnd), unwired); cursor = std::max(cursor, lockedEnd); } if (cursor < end) unwired.emplace_back(cursor, end); @@ -778,10 +811,18 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { if (!GuestRangeMapped(start, end)) return SCE_KERNEL_ERROR_ENOMEM; +#ifdef __APPLE__ + std::lock_guard lock(g_lockedRangesLock); + if (::munlock(reinterpret_cast<void*>(start), end - start) == 0) { + ForgetLockedRangeLocked(start, end); + return 0; + } +#else if (::munlock(reinterpret_cast<void*>(start), end - start) == 0) { ForgetLockedRange(start, end); return 0; } +#endif const int error = errno; if (error == ENOMEM) return SCE_KERNEL_ERROR_ENOMEM; throw std::system_error(error, std::generic_category(), "munlock failed"); @@ -816,6 +857,11 @@ int APS5_VABI sceKernelMunlockall_nid_postfix() { { std::lock_guard lock(g_lockedRangesLock); ranges.swap(g_lockedRanges); +#ifdef __APPLE__ + const int saved = errno; + for (const auto& [start, len] : ranges) ::munlock(reinterpret_cast<void*>(start), len); + errno = saved; +#endif } #ifdef _WIN32 for (const auto& [start, len] : ranges) { @@ -835,10 +881,6 @@ int APS5_VABI sceKernelMunlockall_nid_postfix() { cursor = next; } } -#elif defined(__APPLE__) - const int saved = errno; - for (const auto& [start, len] : ranges) ::munlock(reinterpret_cast<void*>(start), len); - errno = saved; #endif return 0; } diff --git a/core/libs/tests/GuestMlock.cpp b/core/libs/tests/GuestMlock.cpp index 946afc19e2..1e001db318 100644 --- a/core/libs/tests/GuestMlock.cpp +++ b/core/libs/tests/GuestMlock.cpp @@ -90,6 +90,11 @@ int main() { Require(mlock_nid_postfix(mapped, length) == 0); #ifdef __APPLE__ Require(WiredCount(mapped) == 1); + void* remapped = mapped; + Require(sceKernelMapFlexibleMemory(&remapped, length, 3, 0x10) == 0 && remapped == mapped); + Require(WiredCount(mapped) == 0); + Require(mlock_nid_postfix(mapped, length) == 0); + Require(WiredCount(mapped) == 1); #endif bytes[length - 1] = 7; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index aecaed82cc..dd0bb6451c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -136,7 +136,7 @@ - [Exception runtime on macOS](../../../core/libs/prx/libc/src/exception/Exports.cpp) (libc) - on macOS an exception that a library throws with a plain `throw` and that nothing catches skips the guest's `std::set_terminate` handler and the guest runtime's report: it ends in libc++abi's terminate, because the host keeps libc++abi there, while on Linux and Windows libc's runtime takes these throws. The `SIGALRM` expiry of `alarm` and `setitimer` throws through the guest runtime on macOS; `NotImplemented_nid_no_patch` cannot, since host code catches its `std::runtime_error`, so an unimplemented call inside a `noexcept` export (`mmap` on an `app0.zar` file) bypasses the guest handler - 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 (file reads and socket receives), as [Host writes into guest memory](../CONVENTIONS.md#host-writes-into-guest-memory) requires; 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 has to do it through `GuestWriteWatchProtect`, which changes the host protection and the recorded one under the watch's lock, as `mprotect` in libkernel does; code that maps over them has to call `GuestWriteWatchProtectionChanged`. 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, as `sceKernelVirtualQuery` does for memory outside the guest allocation registry; + - code that changes the protection of watched pages has to do it through `GuestWriteWatchProtect`, which changes the host protection and the recorded one under the watch's lock, as `mprotect` in libkernel does. Code that maps over them registers the range again (`GuestWriteWatchRegister`), which reads the new host protection; `GuestWriteWatchProtectionChanged`, for code that changes a protection itself, has no caller yet. 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, as `sceKernelVirtualQuery` does for memory outside the guest allocation registry; - 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. - On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [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` From 62d80b578fe515274017312ba38adb326ee72f86 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:16:26 +0100 Subject: [PATCH 387/702] fix(libkernel): protect guest stack pages through the write watch on macOS --- .../prx/libkernel/DirectMemory/DirectMemory.cpp | 13 +++++++++++-- docs/dev/debt/libkernel.md | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index d107524192..00e98ffb1a 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1361,10 +1361,19 @@ int DoMprotect(const void* addr, size_t len, int prot) { if (nativeProtection != PROT_NONE && hostStack) CommitReservedPages(insidePointer, insideBytes); const bool protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; #else - bool protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; +#ifdef __APPLE__ + const auto protectInside = [&] { + const int error = GuestWriteWatch::GuestWriteWatchProtect_nid_postfix(insidePointer, insideBytes, nativeProtection); + errno = error; + return error == 0; + }; +#else + const auto protectInside = [&] { return mprotect(insidePointer, insideBytes, nativeProtection) == 0; }; +#endif + bool protectedPages = protectInside(); if (!protectedPages && errno == ENOMEM) { MapUnmappedPages(insidePointer, static_cast<std::size_t>((stackEnd & ~hostPageMask) - inside)); - protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; + protectedPages = protectInside(); } #endif if (!protectedPages) throw std::system_error(errno, std::generic_category(), "mprotect failed"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2446220e3e..1fcca805ad 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -259,7 +259,8 @@ - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:<port>` stays `127.0.0.1:<port>`), because a bound socket cannot be bound again, and `getsockname` reports that host address. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), and on a connected datagram socket it disconnects first, as on a PS5 (firmware 13.60) and in FreeBSD 11.4's `soconnect()`. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 (measured for an unbound socket) unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There the call returns the length without the host, as Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range fails with `EBUSY`, as it did in a payload process on a PS5 (firmware 13.60) for anonymous memory, locked or not, also with a hole unmapped inside it; a title process, reservations, direct memory and flexible memory were not measured -- Guest memory protection on macOS ([mprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp), [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): the guest rounds a range to its 16 KiB pages, counted from where the ELF address 0 of the image holding it was loaded, which is a multiple of the host page size only (4 KiB under Rosetta). A rounded edge smaller than a guest page at the start of the request that lies in the gap between two segments of an image, where no host page exists, is left out. For memory outside the guest allocation registry `sceKernelVirtualQuery` reports the region `mach_vm_region` returns, so its extent is the host's, and takes a watched page's write permission from the write watch +- Guest memory protection on macOS ([mprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp), [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): a rounded edge smaller than a guest page at the start of a request that lies in the gap between two segments of an image, where no host page exists, is skipped instead of protected, and for memory outside the guest allocation registry `sceKernelVirtualQuery` reports the host's extent instead of the guest's: the region `mach_vm_region` returns, cut to the write watch run that holds the address. The guest rounds a range to its 16 KiB pages, counted from where the ELF address 0 of the image holding it was loaded, which is a multiple of the host page size only (4 KiB under Rosetta). `sceKernelVirtualQuery` takes a watched page's write permission from the write watch. +- Locked memory on macOS ([mlock, munlock, munlockall](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): a wire that other host code makes on a page of a range libkernel recorded counts as libkernel's, so a later guest `mlock` adds none there and a guest `munlock` removes it. libkernel holds at most one wire per page, and since macOS has no `munlockall`, `sceKernelMunlockall` unlocks each recorded range with one `munlock`. - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it - [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). XPSemu (PPSA97358) imports `mlockall` and `munlockall`; their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. From 896ae02ab58ca6c1cb681433561160a6340a6e9e Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:22:30 +0100 Subject: [PATCH 388/702] fix(libkernel): address the macOS stack protection review --- core/libs/prx/libc/src/GuestAllocations.cpp | 5 +++-- .../prx/libkernel/DirectMemory/DirectMemory.cpp | 17 ++++++----------- docs/dev/debt/libkernel.md | 4 ++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 58d240f166..6118beb55d 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -539,8 +539,9 @@ void GuestAllocationsGuestPages_nid_postfix(std::uintptr_t address, std::size_t auto& found = *static_cast<Search*>(data); for (unsigned index = 0; index < image->dlpi_phnum; ++index) { const auto& header = image->dlpi_phdr[index]; - const auto start = static_cast<std::uintptr_t>(image->dlpi_addr + header.p_vaddr); - const auto finish = (start + header.p_memsz + found.hostPage - 1) & ~(found.hostPage - 1); + const auto segment = static_cast<std::uintptr_t>(image->dlpi_addr + header.p_vaddr); + const auto start = segment & ~(found.hostPage - 1); + const auto finish = (segment + header.p_memsz + found.hostPage - 1) & ~(found.hostPage - 1); if (header.p_type == PT_LOAD && found.address >= start && found.address < finish) { found.bias = static_cast<std::uintptr_t>(image->dlpi_addr); return 1; diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 00e98ffb1a..2f7f5461a5 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1360,20 +1360,15 @@ int DoMprotect(const void* addr, size_t len, int prot) { #ifdef _WIN32 if (nativeProtection != PROT_NONE && hostStack) CommitReservedPages(insidePointer, insideBytes); const bool protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; +#elif defined(__APPLE__) + const int error = GuestWriteWatch::GuestWriteWatchProtect_nid_postfix(insidePointer, insideBytes, nativeProtection); + if (error != 0) errno = error; + const bool protectedPages = error == 0; #else -#ifdef __APPLE__ - const auto protectInside = [&] { - const int error = GuestWriteWatch::GuestWriteWatchProtect_nid_postfix(insidePointer, insideBytes, nativeProtection); - errno = error; - return error == 0; - }; -#else - const auto protectInside = [&] { return mprotect(insidePointer, insideBytes, nativeProtection) == 0; }; -#endif - bool protectedPages = protectInside(); + bool protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; if (!protectedPages && errno == ENOMEM) { MapUnmappedPages(insidePointer, static_cast<std::size_t>((stackEnd & ~hostPageMask) - inside)); - protectedPages = protectInside(); + protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; } #endif if (!protectedPages) throw std::system_error(errno, std::generic_category(), "mprotect failed"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 1fcca805ad..5c7e87eb14 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -260,10 +260,10 @@ - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), and on a connected datagram socket it disconnects first, as on a PS5 (firmware 13.60) and in FreeBSD 11.4's `soconnect()`. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 (measured for an unbound socket) unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There the call returns the length without the host, as Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range fails with `EBUSY`, as it did in a payload process on a PS5 (firmware 13.60) for anonymous memory, locked or not, also with a hole unmapped inside it; a title process, reservations, direct memory and flexible memory were not measured - Guest memory protection on macOS ([mprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp), [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): a rounded edge smaller than a guest page at the start of a request that lies in the gap between two segments of an image, where no host page exists, is skipped instead of protected, and for memory outside the guest allocation registry `sceKernelVirtualQuery` reports the host's extent instead of the guest's: the region `mach_vm_region` returns, cut to the write watch run that holds the address. The guest rounds a range to its 16 KiB pages, counted from where the ELF address 0 of the image holding it was loaded, which is a multiple of the host page size only (4 KiB under Rosetta). `sceKernelVirtualQuery` takes a watched page's write permission from the write watch. -- Locked memory on macOS ([mlock, munlock, munlockall](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): a wire that other host code makes on a page of a range libkernel recorded counts as libkernel's, so a later guest `mlock` adds none there and a guest `munlock` removes it. libkernel holds at most one wire per page, and since macOS has no `munlockall`, `sceKernelMunlockall` unlocks each recorded range with one `munlock`. +- Locked memory on macOS ([mlock, munlock, munlockall](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): a wire that other host code makes on a page of a range libkernel recorded counts as libkernel's, so a later guest `mlock` adds none there and a guest `munlock` removes it. libkernel holds at most one wire per page, and since macOS's `munlockall` fails with `ENOSYS`, `sceKernelMunlockall` unlocks each recorded range with one `munlock`. - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it -- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). XPSemu (PPSA97358) imports `mlockall` and `munlockall`; their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. +- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux; macOS is under Locked memory on macOS). XPSemu (PPSA97358) imports `mlockall` and `munlockall`; their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. - [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. In a payload process on a PS5 (firmware 13.60) `SOCK_RDM`, `SOCK_SEQPACKET` and an unknown type failed with `EPROTOTYPE` when the protocol was 0 and `EPROTONOSUPPORT` otherwise, as here, and a family other than `AF_INET`, `AF_INET6` and `AF_UNIX` with `EAFNOSUPPORT`. The console also created `SOCK_RAW` (for that payload process, which may hold privileges a title does not), `SOCK_DGRAM` with `IPPROTO_UDPLITE`, and `AF_UNIX` stream, datagram and seqpacket sockets; these fail here with `EPROTONOSUPPORT` and `EAFNOSUPPORT` (`socketpair` supports `AF_UNIX`) - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point From e5297264e3566df2a5a2985ee72f9fbe79f311e6 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:05:02 +0100 Subject: [PATCH 389/702] fix(relinker): reject macOS fixups outside writable segments The macOS image writer emitted dyld rebase and bind opcodes for any target inside a segment, so an R_X86_64_RELATIVE into an R-X PT_LOAD became a rebase into a read-execute segment. dyld-1378 refuses fixups in segments without write access (enforceFixupsInWritableSegments), so Locate now throws when the target segment lacks VM_PROT_WRITE. Such inputs now fail the macOS conversion instead of writing an output that dyld would not load. --- .../elfpatcher/src/macos/MacOsImage.cpp | 1 + .../tests/test_interpreter_in_code.py | 2 +- .../relinker/tests/test_macos_output.py | 65 +++++++++++++++++++ docs/dev/debt/relinker.md | 1 + 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index d539683a1e..c98e30d3b9 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -224,6 +224,7 @@ std::pair<std::uint8_t, std::uint64_t> Locate(const std::vector<Segment>& segmen const auto& segment = segments[index]; if (segment.MemorySize == 0 || rva < segment.Rva || rva - segment.Rva >= segment.MemorySize || size > segment.MemorySize - (rva - segment.Rva)) continue; if (index > 15) throw Domain::RelinkerException("Too many Mach-O segments for dyld opcodes", rva); + if ((segment.Protection & VmProtWrite) == 0) throw Domain::RelinkerException("Mach-O fixup target is not in a writable segment", rva); return {static_cast<std::uint8_t>(index), rva - segment.Rva}; } throw Domain::RelinkerException("Mach-O fixup target is outside the image", rva); diff --git a/core/relinker/relinker/tests/test_interpreter_in_code.py b/core/relinker/relinker/tests/test_interpreter_in_code.py index 7ec2c4ceba..5fe57d8805 100644 --- a/core/relinker/relinker/tests/test_interpreter_in_code.py +++ b/core/relinker/relinker/tests/test_interpreter_in_code.py @@ -24,7 +24,7 @@ def fixture(interpreter=True, trailing=False): tags = [(5, DATA_ADDRESS + 0x600), (10, 1), (6, DATA_ADDRESS + 0x620), (11, 24), (7, DATA_ADDRESS + 0x700), (8, 24), (9, 24), (0, 0)] struct.pack_into("<IIQQQQQQ", image, 64, - 1, 4, 0, DATA_ADDRESS, DATA_ADDRESS, CODE_OFFSET, CODE_OFFSET, 0x1000) + 1, 6, 0, DATA_ADDRESS, DATA_ADDRESS, CODE_OFFSET, CODE_OFFSET, 0x1000) struct.pack_into("<IIQQQQQQ", image, 120, 1, 5, CODE_OFFSET, 0, 0, 0x1000, 0x1000, 0x1000) struct.pack_into("<IIQQQQQQ", image, 176, 2, 6, 0x400, DATA_ADDRESS + 0x400, diff --git a/core/relinker/relinker/tests/test_macos_output.py b/core/relinker/relinker/tests/test_macos_output.py index fd9407a851..7b21219c05 100644 --- a/core/relinker/relinker/tests/test_macos_output.py +++ b/core/relinker/relinker/tests/test_macos_output.py @@ -205,6 +205,70 @@ def check_runpath_lists(relinker, directory): check(not output.exists(), "empty run path created an output") +def uleb(data, offset): + value = shift = 0 + while True: + byte = data[offset] + offset += 1 + value |= (byte & 0x7F) << shift + shift += 7 + if byte < 0x80: + return value, offset + + +def rebases(data): + macho = MachO(data) + offset, end = macho.dyld_info[0], macho.dyld_info[0] + macho.dyld_info[1] + found = [] + segment = address = 0 + while offset < end: + opcode, immediate = data[offset] & 0xF0, data[offset] & 0x0F + offset += 1 + if opcode == 0x00: + break + if opcode == 0x20: + segment = immediate + address, offset = uleb(data, offset) + elif opcode == 0x30: + delta, offset = uleb(data, offset) + address += delta + elif opcode == 0x40: + address += immediate * 8 + elif opcode in (0x50, 0x60, 0x70): + count = immediate if opcode == 0x50 else 1 + if opcode == 0x60: + count, offset = uleb(data, offset) + for _ in range(count): + found.append((macho.segments[segment]["name"], address)) + address += 8 + if opcode == 0x70: + delta, offset = uleb(data, offset) + address += delta + elif opcode != 0x10: + raise SystemExit(f"rebase opcode {opcode:#x} is not decoded") + return found + + +def check_fixup_protection(relinker, directory): + for name, target in (("fixup-data", 0x5200), ("fixup-code", 0x4100)): + image = import_fixture() + struct.pack_into("<QQq", image, 0x5C30, target, 8, 0x5100) + struct.pack_into("<qQ", image, 0x5800 + 7 * 16, 8, 72) + source = Path(directory) / f"{name}.elf" + output = Path(directory) / name + source.write_bytes(bytes(image)) + result = subprocess.run([str(relinker), "--macos", "--skip-sce-module", str(source), str(output)], + capture_output=True, text=True, timeout=20) + if target == 0x4100: + check(result.returncode == 2 and "FAIL: Mach-O fixup target is not in a writable segment (offset 0x10100)" in result.stderr, + f"{name}: rebase into a non-writable segment accepted: {result.returncode} {result.stdout}{result.stderr}") + check(not output.exists(), f"{name}: rejected fixup created an output") + else: + check(result.returncode == 0, f"{name}: {result.stdout}{result.stderr}") + found = rebases(output.read_bytes()) + check(("__ELF1", 0x200) in found, f"{name}: no rebase at __ELF1+0x200 in {found}") + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-macos-output-") as directory: @@ -213,6 +277,7 @@ def main(): check_tls(relink(relinker, directory, "tls", tls_fixture())) check_options(relinker, directory) check_runpath_lists(relinker, directory) + check_fixup_protection(relinker, directory) print("macOS output checks passed") diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 7850bd1839..d607c80db1 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -39,6 +39,7 @@ - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - In Windows output, an executable relocation against a named import whose 8 bytes overlap the `PT_TLS` initialization image fails the conversion ([PE patcher](../../../core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp)), as it does in a guest module. ld.so applies such a relocation before it copies the image into a thread's TLS block, so every thread reads the bound value. The `.gtls` template is copied from the image at conversion time, and the loader allocates the main thread's block from it before the entry stub binds imports; binding would require the stub to write the value into both the template and that block. No title with such a relocation is known. - In macOS output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. +- macOS output still writes pointer fixups into executable segments, which dyld refuses under the same policy that requires fixups in writable segments; the [macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) rejects only targets in a segment without write access. A segment is executable and writable when its `PT_LOAD` is `PF_R|PF_W|PF_X`, or when a writable `PT_LOAD` shares a 4 KiB page with an executable one, because each page takes the union of the flags of the `PT_LOAD`s that cover it. Not run on macOS. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. 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. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-rosetta` output faults on an M2 running macOS 14.8.9 when it reaches AVX, AVX2, FMA, F16C, RDRAND or a BMI1/BMI2 instruction other than ANDN, TZCNT and LZCNT: Rosetta on that host raises SIGILL for them and for RDSEED, RDPID, CLWB, ADCX and ADOX ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Only TZCNT, LZCNT, POPCNT, MOVBE, CLFLUSHOPT, PREFETCHW and RDTSCP execute, and RDTSCP sets ECX to 0xAAAAAAAB. CPUID leaf 1 reports POPCNT and none of MOVBE, AVX, FMA, F16C or RDRAND, leaf 7 reads zero, and leaf 0x80000001 reports RDTSCP but neither LZCNT nor PREFETCHW. An M2 Max on macOS 27.2 reports the same CPUID but executes everything except RDSEED, RDPID, CLWB, ADCX and ADOX, so the faults follow the macOS version, not the chip. From 03268452574893eb5b8a7daee4a341f799bd0b3b Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:37:29 +0100 Subject: [PATCH 390/702] docs(relinker): record measured dyld behaviour for macOS fixups The debt entry on pointer fixups in executable segments claimed that dyld refuses them, which no run had shown. It now states what was measured on an M1 Pro running macOS 27.0.1 under Rosetta 2: dyld_info reports both cases, the PF_R|PF_W|PF_X case loads and runs, and a fixup into a PF_R|PF_X segment ends with SIGBUS in dyld. The interpreter_in_code fixture keeps a read-only data PT_LOAD for the Windows runs, so PE output still covers a RELATIVE into it. --- core/relinker/relinker/tests/test_interpreter_in_code.py | 6 +++--- docs/dev/debt/relinker.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/relinker/relinker/tests/test_interpreter_in_code.py b/core/relinker/relinker/tests/test_interpreter_in_code.py index 5fe57d8805..7090e2a0c0 100644 --- a/core/relinker/relinker/tests/test_interpreter_in_code.py +++ b/core/relinker/relinker/tests/test_interpreter_in_code.py @@ -13,7 +13,7 @@ ENTRY = 0x100 TRAILING_ADDRESS = 0xF00 -def fixture(interpreter=True, trailing=False): +def fixture(interpreter=True, trailing=False, data_flags=4): image = bytearray(0x2000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) struct.pack_into("<HHIQQQIHHHHHH", image, 16, @@ -24,7 +24,7 @@ def fixture(interpreter=True, trailing=False): tags = [(5, DATA_ADDRESS + 0x600), (10, 1), (6, DATA_ADDRESS + 0x620), (11, 24), (7, DATA_ADDRESS + 0x700), (8, 24), (9, 24), (0, 0)] struct.pack_into("<IIQQQQQQ", image, 64, - 1, 6, 0, DATA_ADDRESS, DATA_ADDRESS, CODE_OFFSET, CODE_OFFSET, 0x1000) + 1, data_flags, 0, DATA_ADDRESS, DATA_ADDRESS, CODE_OFFSET, CODE_OFFSET, 0x1000) struct.pack_into("<IIQQQQQQ", image, 120, 1, 5, CODE_OFFSET, 0, 0, 0x1000, 0x1000, 0x1000) struct.pack_into("<IIQQQQQQ", image, 176, 2, 6, 0x400, DATA_ADDRESS + 0x400, @@ -47,7 +47,7 @@ def main(): for name, interpreter, trailing in (("interpreter", True, False), ("plain", False, False), ("trailing", False, True)): source = work / (name + target + ".elf") output = work / (name + target + suffix) - source.write_bytes(fixture(interpreter, trailing)) + source.write_bytes(fixture(interpreter, trailing, 6 if target == "--macos" else 4)) result = subprocess.run([str(relinker), "--skip-sce-module", target, str(source), str(output)], capture_output=True, text=True, timeout=20) if result.returncode != 0 or not output.exists(): diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index d607c80db1..ba5e420fd1 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -39,7 +39,7 @@ - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - In Windows output, an executable relocation against a named import whose 8 bytes overlap the `PT_TLS` initialization image fails the conversion ([PE patcher](../../../core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp)), as it does in a guest module. ld.so applies such a relocation before it copies the image into a thread's TLS block, so every thread reads the bound value. The `.gtls` template is copied from the image at conversion time, and the loader allocates the main thread's block from it before the entry stub binds imports; binding would require the stub to write the value into both the template and that block. No title with such a relocation is known. - In macOS output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. -- macOS output still writes pointer fixups into executable segments, which dyld refuses under the same policy that requires fixups in writable segments; the [macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) rejects only targets in a segment without write access. A segment is executable and writable when its `PT_LOAD` is `PF_R|PF_W|PF_X`, or when a writable `PT_LOAD` shares a 4 KiB page with an executable one, because each page takes the union of the flags of the `PT_LOAD`s that cover it. Not run on macOS. +- macOS output still writes pointer fixups into executable segments. `dyld_info -fixups` reports each as `pointer rebase is in executable segment`, next to `pointer rebase is in non-writable segment` for a fixup in a segment without write access, which the [macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) rejects. A segment is executable and writable when its `PT_LOAD` is `PF_R|PF_W|PF_X`, or when a writable `PT_LOAD` shares a 4 KiB page with an executable one, because each page takes the union of the flags of the `PT_LOAD`s that cover it. dyld on an M1 Pro running macOS 27.0.1 (x86-64 output under Rosetta 2, `LC_BUILD_VERSION` minos and sdk 11.0) loads such an executable, applies the fixup and runs it; a fixup into a `PF_R|PF_X` segment ended the process there with SIGBUS in `dyld4::Loader::applyFixupsGeneric` before `main`, not with a refusal. Not measured for a dylib or under another macOS or SDK version. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. 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. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-rosetta` output faults on an M2 running macOS 14.8.9 when it reaches AVX, AVX2, FMA, F16C, RDRAND or a BMI1/BMI2 instruction other than ANDN, TZCNT and LZCNT: Rosetta on that host raises SIGILL for them and for RDSEED, RDPID, CLWB, ADCX and ADOX ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Only TZCNT, LZCNT, POPCNT, MOVBE, CLFLUSHOPT, PREFETCHW and RDTSCP execute, and RDTSCP sets ECX to 0xAAAAAAAB. CPUID leaf 1 reports POPCNT and none of MOVBE, AVX, FMA, F16C or RDRAND, leaf 7 reads zero, and leaf 0x80000001 reports RDTSCP but neither LZCNT nor PREFETCHW. An M2 Max on macOS 27.2 reports the same CPUID but executes everything except RDSEED, RDPID, CLWB, ADCX and ADOX, so the faults follow the macOS version, not the chip. From 9e66ff6b20869a7081840a330aab17e050139933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Tue, 27 Oct 2026 08:19:05 +0100 Subject: [PATCH 391/702] test(relinker): drop the RELATIVE fixup from the strict dynamic segment fixture Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/relinker/relinker/tests/test_dynamic_segment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/relinker/relinker/tests/test_dynamic_segment.py b/core/relinker/relinker/tests/test_dynamic_segment.py index 5bbe2b628e..699231cabc 100644 --- a/core/relinker/relinker/tests/test_dynamic_segment.py +++ b/core/relinker/relinker/tests/test_dynamic_segment.py @@ -21,6 +21,7 @@ def fixture(): def strict_fixture(): image = optional_plt_fixture([(4, 0x6a0)]) struct.pack_into("<I", image, 64 + 4, 5) + struct.pack_into("<Q", image, 0x400 + 5 * 16 + 8, 0) struct.pack_into("<IIII", image, 0x6a0, 1, 1, 0, 0) return image From 4d8fa35a5e593e452a97ee0f97aabbd63eb31345 Mon Sep 17 00:00:00 2001 From: Gruzzberry <angus@hutchisons.org> Date: Sun, 25 Oct 2026 22:33:26 +0000 Subject: [PATCH 392/702] fix(libkernel): fail sysctl and sysctlbyname with EFAULT on an unreadable or unwritable pointer FreeBSD 11.4's sys___sysctl and userland_sysctl copy the name and the old length in, and copy the value and the length out, so a bad pointer gives EFAULT in that order. sysctl dereferenced them directly, and sysctlbyname read its name with strcmp. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/General.cpp | 7 +++ .../System/src/SystemConfiguration.cpp | 42 +++++++++------- core/libs/tests/GuestSystemConfiguration.cpp | 50 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 5 files changed, 84 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 33499fd85e..9ce5bb8cb3 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -33,6 +33,7 @@ extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& desti extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, std::size_t bytes); +extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, std::size_t bytes); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); extern "C" std::filesystem::path ResolveDiskPath_nid_no_patch(const char* path); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 9c68872826..972d2f2070 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -845,6 +845,13 @@ extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, #endif } +extern "C" bool GuestCopyIn_nid_no_patch(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + const auto start = reinterpret_cast<std::uintptr_t>(source); + if (start == 0 || bytes > UINTPTR_MAX - start) return false; + return CopyGuestBytes(destination, source, bytes); +} + extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { auto resolved = ResolveLocked(path, false, true); AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index b6c40105c9..89fa37de98 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -27,14 +28,21 @@ namespace { +int Fault() { + *__error_nid_postfix() = 14; + return -1; +} + int CopyOut(const void* value, std::size_t size, void* oldValue, std::size_t* oldLength) { if (oldValue == nullptr) { - if (oldLength != nullptr) *oldLength = size; + if (oldLength != nullptr && !GuestCopyOut_nid_no_patch(oldLength, &size, sizeof(size))) return Fault(); return 0; } - const std::size_t copied = std::min(oldLength != nullptr ? *oldLength : 0, size); - if (copied != 0) std::memcpy(oldValue, value, copied); - if (oldLength != nullptr) *oldLength = copied; + std::size_t capacity = 0; + if (oldLength != nullptr && !GuestCopyIn_nid_no_patch(&capacity, oldLength, sizeof(capacity))) return Fault(); + const std::size_t copied = std::min(capacity, size); + if (!GuestCopyOut_nid_no_patch(oldValue, value, copied)) return Fault(); + if (oldLength != nullptr && !GuestCopyOut_nid_no_patch(oldLength, &copied, sizeof(copied))) return Fault(); if (copied < size) { *__error_nid_postfix() = 12; return -1; @@ -299,10 +307,11 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 22; return -1; } - if (name == nullptr) { - *__error_nid_postfix() = 14; - return -1; - } + int guestName[24]; + if (!GuestCopyIn_nid_no_patch(guestName, name, nameLength * sizeof(int))) return Fault(); + name = guestName; + std::size_t capacity = 0; + if (oldLength != nullptr && !GuestCopyIn_nid_no_patch(&capacity, oldLength, sizeof(capacity))) return Fault(); if (IsMeasuredAbsentMib(name, nameLength)) { if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing an unmeasured absent MIB"); *__error_nid_postfix() = 2; @@ -439,13 +448,14 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void return -1; } if (oldLength == nullptr) return 0; - const std::size_t count = std::min(*oldLength, static_cast<std::size_t>(256)); + const std::size_t count = std::min(capacity, static_cast<std::size_t>(256)); if (oldValue != nullptr) { static std::random_device device; - auto* bytes = static_cast<std::uint8_t*>(oldValue); + std::array<std::uint8_t, 256> bytes{}; for (std::size_t i = 0; i < count; ++i) bytes[i] = static_cast<std::uint8_t>(device()); + if (!GuestCopyOut_nid_no_patch(oldValue, bytes.data(), count)) return Fault(); } - *oldLength = count; + if (!GuestCopyOut_nid_no_patch(oldLength, &count, sizeof(count))) return Fault(); return 0; } if (nameLength == 2 && name[0] == 1 && name[1] == 50) { @@ -472,14 +482,12 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void } int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::size_t* oldLength, const void* newValue, std::size_t newLength) { - if (name == nullptr) { - *__error_nid_postfix() = 14; - return -1; - } - if (strnlen(name, MaximumSysctlName) == MaximumSysctlName) { - *__error_nid_postfix() = 63; + std::string guestName; + if (const int error = CopyGuestPath_nid_no_patch(name, guestName)) { + *__error_nid_postfix() = error; return -1; } + name = guestName.c_str(); const std::string trimmed = WithoutTrailingDot(name); name = trimmed.c_str(); if (TopLevelNode(trimmed) >= 0) { diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 9c19aa6e91..f4342f7c6f 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -1010,10 +1010,60 @@ static void CheckUnknownRootsAndTopLevelNames() { *__error_nid_postfix() = 13; Require(sysctlbyname_nid_postfix(tooLong.c_str(), &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); } +static void CheckSysctlPointerFaults() { + constexpr std::size_t page = 0x4000; + void* memory = nullptr; + Require(sceKernelMapFlexibleMemory(&memory, page, 3, 0) == 0 && memory != nullptr); + auto* readOnlyLength = static_cast<std::size_t*>(memory); + *readOnlyLength = sizeof(int); + Require(sceKernelMprotect(memory, page, 1) == 0); + const auto fails = [](int result) { return result == -1 && *__error_nid_postfix() == 14; }; + auto* guard = static_cast<char*>(nullptr); + void* pages = nullptr; + Require(sceKernelMapFlexibleMemory(&pages, page * 2, 3, 0) == 0 && pages != nullptr); + guard = static_cast<char*>(pages) + page; + Require(sceKernelMprotect(guard, page, 0) == 0); + char* crossing = guard - 4; + std::memcpy(crossing, "kern", 4); + std::size_t nameLength = sizeof(int); + int nameValue = 0; + Require(fails(sysctlbyname_nid_postfix(reinterpret_cast<const char*>(0x10), &nameValue, &nameLength, nullptr, 0))); + Require(fails(sysctlbyname_nid_postfix(nullptr, &nameValue, &nameLength, nullptr, 0))); + Require(fails(sysctlbyname_nid_postfix(crossing, &nameValue, &nameLength, nullptr, 0))); + Require(sysctlbyname_nid_postfix("hw.ncpu", &nameValue, &nameLength, nullptr, 0) == 0 && nameValue > 0); + Require(fails(sysctlbyname_nid_postfix("hw.ncpu", &nameValue, reinterpret_cast<std::size_t*>(0x10), nullptr, 0))); + Require(fails(sysctlbyname_nid_postfix("hw.ncpu", reinterpret_cast<void*>(0x10), &nameLength, nullptr, 0))); + const int mib[] = {6, 3}; + const int absent[] = {1, 6}; + const auto unreadable = reinterpret_cast<const int*>(0x10); + const auto badLength = reinterpret_cast<std::size_t*>(0x10); + int value = 0; + std::size_t length = sizeof(value); + Require(fails(sysctl_nid_postfix(unreadable, 2, &value, &length, nullptr, 0))); + Require(fails(sysctl_nid_postfix(nullptr, 2, &value, &length, nullptr, 0))); + Require(fails(sysctl_nid_postfix(mib, 2, &value, badLength, nullptr, 0))); + Require(fails(sysctl_nid_postfix(absent, 2, &value, badLength, nullptr, 0))); + Require(fails(sysctl_nid_postfix(mib, 2, reinterpret_cast<void*>(0x10), &length, nullptr, 0)) && length == sizeof(value)); + Require(fails(sysctl_nid_postfix(mib, 2, nullptr, badLength, nullptr, 0))); + Require(fails(sysctl_nid_postfix(mib, 2, &value, readOnlyLength, nullptr, 0)) && value != 0); + Require(fails(sysctl_nid_postfix(mib, 2, nullptr, readOnlyLength, nullptr, 0))); + length = 0; + Require(sysctl_nid_postfix(mib, 2, reinterpret_cast<void*>(0x10), &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12 && length == 0); + Require(sysctl_nid_postfix(unreadable, 1, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + Require(sysctl_nid_postfix(unreadable, 25, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + Require(sysctl_nid_postfix(absent, 2, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + const int arandom[] = {1, 37}; + std::uint8_t bytes[8] = {}; + length = sizeof(bytes); + Require(fails(sysctl_nid_postfix(arandom, 2, reinterpret_cast<void*>(0x10), &length, nullptr, 0)) && length == sizeof(bytes)); + Require(fails(sysctl_nid_postfix(arandom, 2, bytes, badLength, nullptr, 0))); + Require(sysctl_nid_postfix(arandom, 2, bytes, &length, nullptr, 0) == 0 && length == sizeof(bytes)); +} int main() { CheckUnknownRootsAndTopLevelNames(); CheckProcessorCountSysctl(); + CheckSysctlPointerFaults(); CheckPageSizeSysctl(); CheckPhysicalMemorySysctl(5, "hw.physmem"); CheckPhysicalMemorySysctl(12, "hw.realmem"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index bf0ae0d386..1351f62d1a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -312,3 +312,4 @@ - [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured - [flock on a directory descriptor](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) returns 0 on Windows without taking a lock, as FreeBSD 11.4's `sys_flock` accepts any vnode descriptor and so succeeds on a directory; Windows `LockFileEx` rejects a directory handle, which used to abort the process. Two descriptors of the same directory therefore never conflict: a second `LOCK_EX | LOCK_NB` succeeds where FreeBSD fails with `EWOULDBLOCK`. Linux and macOS use the host `flock`. Not measured on a console - [poll on macOS and urgent data](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - XNU's `poll` never reports `POLLPRI` for out-of-band data. On macOS it is taken from `SIOCATMARK`, which reports a pending urgent byte without consuming it, and a wait that asks for it uses the sliced path a kqueue wait uses, so it wakes on the next 1 ms slice; `POLLRDBAND` is set with it, as FreeBSD 11.4's `sopoll_generic` does. The other paths that ask the host for urgent readiness still miss it on macOS: guest `select` and `pselect` through `GuestSockets::Poll`, and libSceNet's `PollSockets`. `recv(MSG_OOB | MSG_PEEK)` is not the probe because it consumes the byte on macOS 27.0.1, and `kevent` with `NOTE_OOB` does not fire there. Every other flag and error comes from `poll`; every other host is unchanged. The guest `send` and `recv` refuse `MSG_OOB`, so urgent data can only come from an outside peer; the console's stop once the normal stream passes the mark was not reproduced +- [sysctlnametomib](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) still reads its `name` with `strnlen` and `strcmp` and its `size` pointer directly, so an unmapped pointer faults the host. FreeBSD 11.4's libc `sysctlnametomib` (which `sysctlbyname` calls) runs `strlen` on the name and reads and writes `*size` in user space before the syscall, so an unmapped name or size crashes a console process too; only the `mib` copy-out gives `EFAULT`, and that is not modelled here. `sysctlbyname` copies its name safely and returns `EFAULT` instead of crashing, and `sysctl` checks its pointers From c6522d4bce81ff4a9cf307ac8c2aa8657a20f3c1 Mon Sep 17 00:00:00 2001 From: JaceTheBlu <xngt974@gmail.com> Date: Sun, 25 Oct 2026 22:55:19 +0000 Subject: [PATCH 393/702] feat(libSceLibreSsl): add the LibreSSL object table and implement i2t_ASN1_OBJECT i2t_ASN1_OBJECT threw and was declared without parameters. It now prints an object's long name, or its short name, or its dotted OID, as OBJ_obj2txt does in LibreSSL 2.5.4, the version string the module holds: the NID is the object's own or found by the same binary search over the same order, an unassigned NID reports OBJ_R_UNKNOWN_NID for each name lookup, a truncated subidentifier returns 0 after the parts already written, and arcs past 64 bits are printed exactly. The 956 objects (short and long names, NID, OID) and their search order are generated from 2.5.4's objects.txt and obj_mac.num with its own objects.pl and obj_dat.pl, so d2i code can later hand out the same static objects LibreSSL does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 15 + core/libs/prx/libSceLibreSsl/Export.cpp | 5 - core/libs/prx/libSceLibreSsl/include/Api.hpp | 11 +- .../libSceLibreSsl/include/ObjectTable.hpp | 20 + .../prx/libSceLibreSsl/src/ObjectTable.cpp | 1419 +++++++++++++++++ .../prx/libSceLibreSsl/src/ObjectText.cpp | 207 +++ .../prx/libSceLibreSsl/tests/ObjectText.cpp | 138 ++ docs/dev/debt/libSceLibreSsl.md | 4 +- 8 files changed, 1812 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/include/ObjectTable.hpp create mode 100644 core/libs/prx/libSceLibreSsl/src/ObjectTable.cpp create mode 100644 core/libs/prx/libSceLibreSsl/src/ObjectText.cpp create mode 100644 core/libs/prx/libSceLibreSsl/tests/ObjectText.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 3621baad39..ab5af3ae25 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -178,3 +178,18 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>") endif() endif() + +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ObjectTable.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ObjectText.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_object_text_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/ObjectText.cpp) + target_include_directories(guest_libressl_object_text_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_object_text_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_object_text_tests) + add_test(NAME guest_libressl_object_text COMMAND guest_libressl_object_text_tests) + set_tests_properties(guest_libressl_object_text PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_object_text PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 00ace935b0..b2ab536cf3 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1708,11 +1708,6 @@ int APS5_VABI i2d_X509_PUBKEY_nid_postfix(void) { return 0; } -int APS5_VABI i2t_ASN1_OBJECT_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceLibreSslGetMemoryPoolStats(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index a114265d0c..478d27f679 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -21,6 +21,15 @@ struct CryptoThreadId { std::uint64_t val; }; +struct LibreSslAsn1Object { + const char* shortName; + const char* longName; + std::int32_t nid; + std::int32_t length; + const unsigned char* data; + std::int32_t flags; +}; + struct Md4Context { std::uint32_t A, B, C, D; std::uint32_t Nl, Nh; @@ -467,7 +476,7 @@ int APS5_VABI d2i_RSA_PUBKEY_nid_postfix(void); int APS5_VABI d2i_X509_nid_postfix(void); int APS5_VABI i2a_ASN1_OBJECT_nid_postfix(void); int APS5_VABI i2d_X509_PUBKEY_nid_postfix(void); -int APS5_VABI i2t_ASN1_OBJECT_nid_postfix(void); +int APS5_VABI i2t_ASN1_OBJECT_nid_postfix(char* buffer, int length, const LibreSslAsn1Object* object); int APS5_VABI sceLibreSslGetMemoryPoolStats(void); int APS5_VABI sceLibreSslInit(void); int APS5_VABI sceLibreSslInitEx(void); diff --git a/core/libs/prx/libSceLibreSsl/include/ObjectTable.hpp b/core/libs/prx/libSceLibreSsl/include/ObjectTable.hpp new file mode 100644 index 0000000000..fa770930f3 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/include/ObjectTable.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBSCELIBRESSL_INCLUDE_OBJECTTABLE_HPP +#define CORE_LIBS_PRX_LIBSCELIBRESSL_INCLUDE_OBJECTTABLE_HPP + +#include "prx/libSceLibreSsl/include/Api.hpp" + +inline constexpr int LibreSslObjectCount = 956; +inline constexpr int LibreSslObjectOrderCount = 890; + +extern "C" { + +extern const LibreSslAsn1Object LibreSslObjects_nid_no_patch[LibreSslObjectCount]; +extern const unsigned short LibreSslObjectOrder_nid_no_patch[LibreSslObjectOrderCount]; + +int LibreSslObjectNid_nid_no_patch(const LibreSslAsn1Object* object); +const char* LibreSslObjectShortName_nid_no_patch(int nid); +const char* LibreSslObjectLongName_nid_no_patch(int nid); + +} + +#endif diff --git a/core/libs/prx/libSceLibreSsl/src/ObjectTable.cpp b/core/libs/prx/libSceLibreSsl/src/ObjectTable.cpp new file mode 100644 index 0000000000..3b7e145615 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/ObjectTable.cpp @@ -0,0 +1,1419 @@ +#include "prx/libSceLibreSsl/include/ObjectTable.hpp" + +namespace { + +constexpr unsigned char objectData[6216] = { + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x02, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x03, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x04, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x05, 0x03, 0x55, 0x55, 0x04, 0x55, 0x04, 0x03, 0x55, 0x04, 0x06, 0x55, 0x04, 0x07, 0x55, 0x04, + 0x08, 0x55, 0x04, 0x0a, 0x55, 0x04, 0x0b, 0x55, 0x08, 0x01, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x07, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x07, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x03, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x05, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x06, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x03, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x03, 0x01, 0x2b, 0x0e, 0x03, 0x02, 0x06, 0x2b, + 0x0e, 0x03, 0x02, 0x09, 0x2b, 0x0e, 0x03, 0x02, 0x07, 0x2b, 0x0e, 0x03, 0x02, 0x11, 0x2b, 0x06, + 0x01, 0x04, 0x01, 0x81, 0x3c, 0x07, 0x01, 0x01, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x03, + 0x02, 0x2b, 0x0e, 0x03, 0x02, 0x12, 0x2b, 0x0e, 0x03, 0x02, 0x0f, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x03, 0x07, 0x2b, 0x0e, 0x03, 0x02, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x09, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x03, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x09, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x05, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x06, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x07, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, + 0x09, 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x01, + 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x02, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x0d, 0x2b, 0x0e, 0x03, 0x02, 0x0c, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0b, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x05, 0x0c, 0x2b, 0x0e, 0x03, 0x02, 0x1b, 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x01, 0x01, + 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x01, 0x02, 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, + 0x01, 0x03, 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x01, 0x04, 0x60, 0x86, 0x48, 0x01, 0x86, + 0xf8, 0x42, 0x01, 0x07, 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x01, 0x08, 0x60, 0x86, 0x48, + 0x01, 0x86, 0xf8, 0x42, 0x01, 0x0c, 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x01, 0x0d, 0x60, + 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x02, 0x05, 0x55, 0x1d, 0x55, 0x1d, 0x0e, 0x55, 0x1d, 0x0f, + 0x55, 0x1d, 0x10, 0x55, 0x1d, 0x11, 0x55, 0x1d, 0x12, 0x55, 0x1d, 0x13, 0x55, 0x1d, 0x14, 0x55, + 0x1d, 0x20, 0x55, 0x1d, 0x23, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x02, 0x55, 0x08, + 0x03, 0x65, 0x55, 0x08, 0x03, 0x64, 0x55, 0x04, 0x2a, 0x55, 0x04, 0x04, 0x55, 0x04, 0x2b, 0x55, + 0x1d, 0x1f, 0x2b, 0x0e, 0x03, 0x02, 0x03, 0x55, 0x04, 0x05, 0x55, 0x04, 0x0c, 0x55, 0x04, 0x0d, + 0x2a, 0x86, 0x48, 0x86, 0xf6, 0x7d, 0x07, 0x42, 0x0a, 0x2a, 0x86, 0x48, 0x86, 0xf6, 0x7d, 0x07, + 0x42, 0x0c, 0x2a, 0x86, 0x48, 0xce, 0x38, 0x04, 0x03, 0x2b, 0x0e, 0x03, 0x02, 0x1d, 0x2a, 0x86, + 0x48, 0xce, 0x38, 0x04, 0x01, 0x2b, 0x24, 0x03, 0x02, 0x01, 0x2b, 0x24, 0x03, 0x03, 0x01, 0x02, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x03, 0x08, 0x29, 0x01, 0x01, 0x85, 0x1a, 0x01, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x03, 0x08, 0x55, 0x1d, 0x25, 0x2b, 0x06, 0x01, 0x05, + 0x05, 0x07, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, + 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, + 0x03, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, + 0x08, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x15, 0x2b, 0x06, 0x01, 0x04, 0x01, + 0x82, 0x37, 0x02, 0x01, 0x16, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x0a, 0x03, 0x01, 0x2b, + 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x0a, 0x03, 0x03, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, + 0x0a, 0x03, 0x04, 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x04, 0x01, 0x55, 0x1d, 0x1b, 0x55, + 0x1d, 0x15, 0x55, 0x1d, 0x18, 0x2b, 0x65, 0x01, 0x04, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x0c, 0x01, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x01, 0x02, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x01, 0x03, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, + 0x01, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x01, 0x05, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x0c, 0x01, 0x06, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x0a, 0x01, + 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x0a, 0x01, 0x02, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x0c, 0x0a, 0x01, 0x03, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x0a, + 0x01, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x0a, 0x01, 0x05, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x0a, 0x01, 0x06, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, + 0x14, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x15, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x09, 0x16, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x16, 0x02, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x17, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, + 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0e, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x02, 0x07, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, + 0x02, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x0f, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x05, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x06, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x0e, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x0e, 0x55, 0x04, 0x29, 0x55, 0x04, 0x2e, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x2b, 0x06, 0x01, + 0x05, 0x05, 0x07, 0x01, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x2b, 0x06, 0x01, + 0x05, 0x05, 0x07, 0x30, 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x09, 0x2a, 0x2a, 0x86, + 0x48, 0x2a, 0x86, 0x48, 0xce, 0x38, 0x2a, 0x86, 0x48, 0xce, 0x38, 0x04, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x00, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x09, 0x10, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x03, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, + 0x05, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x06, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x09, 0x10, 0x00, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x00, + 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x00, 0x03, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x00, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, + 0x00, 0x05, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x00, 0x06, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x00, 0x07, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, + 0x10, 0x00, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x01, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x09, 0x10, 0x01, 0x03, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x04, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x05, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x09, 0x10, 0x01, 0x06, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x07, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x09, 0x10, 0x02, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, + 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x03, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, + 0x02, 0x05, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x06, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x07, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, + 0x10, 0x02, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x09, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x0a, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x09, 0x10, 0x02, 0x0b, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x0c, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x09, 0x10, 0x02, 0x0e, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x0f, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x10, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x09, 0x10, 0x02, 0x11, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, + 0x12, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x13, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x14, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, + 0x02, 0x15, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x16, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x17, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, + 0x10, 0x02, 0x18, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x19, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x1a, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x09, 0x10, 0x02, 0x1b, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x1c, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x02, 0x1d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x09, 0x10, 0x03, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x03, 0x02, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x03, 0x03, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x09, 0x10, 0x03, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x03, + 0x05, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x03, 0x06, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x03, 0x07, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, + 0x04, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x05, 0x01, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x05, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, + 0x10, 0x06, 0x01, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x06, 0x02, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x06, 0x03, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x09, 0x10, 0x06, 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x06, 0x05, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x06, 0x06, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x02, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, + 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x05, 0x2b, 0x06, + 0x01, 0x05, 0x05, 0x07, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x2b, 0x06, 0x01, 0x05, + 0x05, 0x07, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x09, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, + 0x0a, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x0b, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x0c, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x02, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x03, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x04, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x05, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x06, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x07, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x08, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x09, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x0a, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x0b, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x0c, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x0d, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x0e, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x0f, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x00, 0x10, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x03, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x05, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x07, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x09, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x03, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x05, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x07, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x0a, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x01, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x03, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x05, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x07, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x09, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x0a, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x0b, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x0c, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x0d, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x0e, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x0f, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x05, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x05, 0x02, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x05, 0x01, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x05, 0x01, + 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x05, 0x01, 0x03, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, + 0x05, 0x01, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x05, 0x01, 0x05, 0x2b, 0x06, 0x01, 0x05, + 0x05, 0x07, 0x05, 0x01, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x05, 0x02, 0x01, 0x2b, 0x06, + 0x01, 0x05, 0x05, 0x07, 0x05, 0x02, 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x06, 0x01, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x06, 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x06, 0x03, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x06, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x01, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x03, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x05, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x07, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x09, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x0a, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x0b, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x0f, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x10, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x11, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x12, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x13, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x15, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x16, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x17, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x07, 0x18, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x01, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x09, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x09, 0x02, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x09, 0x03, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x09, 0x04, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x09, 0x05, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x0a, 0x01, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x0a, 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x0a, 0x03, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x0a, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x0a, 0x05, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x0b, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x0c, 0x01, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x0c, 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x0c, 0x03, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x03, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x04, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, + 0x02, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x03, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, + 0x30, 0x01, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x05, 0x2b, 0x06, 0x01, 0x05, + 0x05, 0x07, 0x30, 0x01, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x07, 0x2b, 0x06, + 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x09, + 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x0a, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, + 0x01, 0x0b, 0x2b, 0x0e, 0x03, 0x02, 0x2b, 0x0e, 0x03, 0x02, 0x0b, 0x55, 0x08, 0x2b, 0x2b, 0x06, + 0x2b, 0x06, 0x01, 0x2b, 0x06, 0x01, 0x01, 0x2b, 0x06, 0x01, 0x02, 0x2b, 0x06, 0x01, 0x03, 0x2b, + 0x06, 0x01, 0x04, 0x2b, 0x06, 0x01, 0x05, 0x2b, 0x06, 0x01, 0x06, 0x2b, 0x06, 0x01, 0x07, 0x2b, + 0x06, 0x01, 0x04, 0x01, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8b, 0x3a, 0x82, 0x58, 0x09, 0x92, 0x26, + 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x19, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, + 0x0d, 0x55, 0x01, 0x05, 0x55, 0x01, 0x05, 0x37, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, + 0x03, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x0a, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, + 0x0b, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x0a, 0x06, 0x55, 0x04, 0x48, 0x55, 0x1d, 0x24, 0x55, + 0x1d, 0x37, 0x55, 0x1d, 0x38, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x01, + 0x01, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x01, 0x02, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x01, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x02, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x03, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x04, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x05, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x06, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x01, 0x2b, 0x06, + 0x01, 0x04, 0x01, 0x82, 0x37, 0x11, 0x01, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x01, + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x02, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, + 0x01, 0x03, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x04, 0x60, 0x86, 0x48, 0x01, 0x65, + 0x03, 0x04, 0x01, 0x15, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x16, 0x60, 0x86, 0x48, + 0x01, 0x65, 0x03, 0x04, 0x01, 0x17, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x18, 0x60, + 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x29, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, + 0x2a, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2b, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x01, 0x2c, 0x55, 0x1d, 0x17, 0x2a, 0x86, 0x48, 0xce, 0x38, 0x02, 0x01, 0x2a, 0x86, 0x48, + 0xce, 0x38, 0x02, 0x02, 0x2a, 0x86, 0x48, 0xce, 0x38, 0x02, 0x03, 0x09, 0x09, 0x92, 0x26, 0x09, + 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x09, 0x92, + 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x03, + 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, + 0x64, 0x0a, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x03, 0x04, 0x09, 0x92, 0x26, 0x89, + 0x93, 0xf2, 0x2c, 0x64, 0x03, 0x05, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x03, + 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x04, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, + 0x2c, 0x64, 0x04, 0x05, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x06, 0x09, 0x92, + 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x07, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, + 0x04, 0x09, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x0e, 0x09, 0x92, 0x26, 0x89, + 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x0f, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x11, + 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x12, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, + 0x2c, 0x64, 0x04, 0x13, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x14, 0x09, 0x92, + 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x04, 0x15, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, + 0x04, 0x16, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x01, 0x09, 0x92, 0x26, 0x89, + 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x02, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x03, + 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x04, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, + 0x2c, 0x64, 0x01, 0x05, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x06, 0x09, 0x92, + 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x07, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, + 0x01, 0x08, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x09, 0x09, 0x92, 0x26, 0x89, + 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x0a, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x0b, + 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x0c, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, + 0x2c, 0x64, 0x01, 0x0d, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x0e, 0x09, 0x92, + 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x0f, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, + 0x01, 0x14, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x15, 0x09, 0x92, 0x26, 0x89, + 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x16, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x17, + 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x18, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, + 0x2c, 0x64, 0x01, 0x1a, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x1b, 0x09, 0x92, + 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x1c, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, + 0x01, 0x1d, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x1e, 0x09, 0x92, 0x26, 0x89, + 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x1f, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x25, + 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x26, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, + 0x2c, 0x64, 0x01, 0x27, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x28, 0x09, 0x92, + 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x29, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, + 0x01, 0x2a, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x2b, 0x09, 0x92, 0x26, 0x89, + 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x2d, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x2e, + 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x2f, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, + 0x2c, 0x64, 0x01, 0x30, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x31, 0x09, 0x92, + 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x32, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, + 0x01, 0x33, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x34, 0x09, 0x92, 0x26, 0x89, + 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x35, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x36, + 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x37, 0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, + 0x2c, 0x64, 0x01, 0x38, 0x55, 0x04, 0x2d, 0x2b, 0x06, 0x01, 0x07, 0x01, 0x2b, 0x06, 0x01, 0x07, + 0x01, 0x01, 0x2b, 0x06, 0x01, 0x07, 0x01, 0x02, 0x2b, 0x06, 0x01, 0x07, 0x01, 0x01, 0x01, 0x2b, + 0x06, 0x01, 0x07, 0x01, 0x01, 0x02, 0x55, 0x04, 0x2c, 0x55, 0x04, 0x41, 0x67, 0x2a, 0x67, 0x2a, + 0x00, 0x67, 0x2a, 0x01, 0x67, 0x2a, 0x03, 0x67, 0x2a, 0x05, 0x67, 0x2a, 0x07, 0x67, 0x2a, 0x08, + 0x67, 0x2a, 0x00, 0x00, 0x67, 0x2a, 0x00, 0x01, 0x67, 0x2a, 0x00, 0x02, 0x67, 0x2a, 0x00, 0x03, + 0x67, 0x2a, 0x00, 0x04, 0x67, 0x2a, 0x00, 0x05, 0x67, 0x2a, 0x00, 0x06, 0x67, 0x2a, 0x00, 0x07, + 0x67, 0x2a, 0x00, 0x08, 0x67, 0x2a, 0x00, 0x09, 0x67, 0x2a, 0x00, 0x0a, 0x67, 0x2a, 0x00, 0x0b, + 0x67, 0x2a, 0x00, 0x0c, 0x67, 0x2a, 0x00, 0x0d, 0x67, 0x2a, 0x00, 0x0e, 0x67, 0x2a, 0x00, 0x10, + 0x67, 0x2a, 0x00, 0x11, 0x67, 0x2a, 0x00, 0x12, 0x67, 0x2a, 0x00, 0x13, 0x67, 0x2a, 0x00, 0x14, + 0x67, 0x2a, 0x00, 0x15, 0x67, 0x2a, 0x00, 0x16, 0x67, 0x2a, 0x00, 0x17, 0x67, 0x2a, 0x00, 0x18, + 0x67, 0x2a, 0x00, 0x19, 0x67, 0x2a, 0x00, 0x1a, 0x67, 0x2a, 0x00, 0x1b, 0x67, 0x2a, 0x00, 0x1c, + 0x67, 0x2a, 0x00, 0x1d, 0x67, 0x2a, 0x00, 0x1e, 0x67, 0x2a, 0x00, 0x1f, 0x67, 0x2a, 0x00, 0x20, + 0x67, 0x2a, 0x00, 0x21, 0x67, 0x2a, 0x00, 0x22, 0x67, 0x2a, 0x00, 0x23, 0x67, 0x2a, 0x00, 0x24, + 0x67, 0x2a, 0x00, 0x25, 0x67, 0x2a, 0x00, 0x26, 0x67, 0x2a, 0x00, 0x27, 0x67, 0x2a, 0x00, 0x28, + 0x67, 0x2a, 0x00, 0x29, 0x67, 0x2a, 0x00, 0x2a, 0x67, 0x2a, 0x00, 0x2b, 0x67, 0x2a, 0x00, 0x2c, + 0x67, 0x2a, 0x00, 0x2d, 0x67, 0x2a, 0x00, 0x2e, 0x67, 0x2a, 0x00, 0x2f, 0x67, 0x2a, 0x00, 0x30, + 0x67, 0x2a, 0x00, 0x31, 0x67, 0x2a, 0x00, 0x32, 0x67, 0x2a, 0x00, 0x33, 0x67, 0x2a, 0x00, 0x34, + 0x67, 0x2a, 0x00, 0x35, 0x67, 0x2a, 0x00, 0x36, 0x67, 0x2a, 0x00, 0x37, 0x67, 0x2a, 0x00, 0x38, + 0x67, 0x2a, 0x00, 0x39, 0x67, 0x2a, 0x00, 0x3a, 0x67, 0x2a, 0x00, 0x3b, 0x67, 0x2a, 0x00, 0x3c, + 0x67, 0x2a, 0x00, 0x3d, 0x67, 0x2a, 0x00, 0x3e, 0x67, 0x2a, 0x00, 0x3f, 0x67, 0x2a, 0x00, 0x40, + 0x67, 0x2a, 0x00, 0x41, 0x67, 0x2a, 0x00, 0x42, 0x67, 0x2a, 0x00, 0x43, 0x67, 0x2a, 0x00, 0x44, + 0x67, 0x2a, 0x00, 0x45, 0x67, 0x2a, 0x00, 0x46, 0x67, 0x2a, 0x00, 0x47, 0x67, 0x2a, 0x00, 0x48, + 0x67, 0x2a, 0x00, 0x49, 0x67, 0x2a, 0x00, 0x4a, 0x67, 0x2a, 0x00, 0x4b, 0x67, 0x2a, 0x00, 0x4c, + 0x67, 0x2a, 0x00, 0x4d, 0x67, 0x2a, 0x00, 0x4e, 0x67, 0x2a, 0x00, 0x4f, 0x67, 0x2a, 0x00, 0x50, + 0x67, 0x2a, 0x00, 0x51, 0x67, 0x2a, 0x00, 0x52, 0x67, 0x2a, 0x01, 0x01, 0x67, 0x2a, 0x01, 0x03, + 0x67, 0x2a, 0x01, 0x04, 0x67, 0x2a, 0x01, 0x05, 0x67, 0x2a, 0x01, 0x07, 0x67, 0x2a, 0x01, 0x08, + 0x67, 0x2a, 0x05, 0x00, 0x67, 0x2a, 0x07, 0x00, 0x67, 0x2a, 0x07, 0x01, 0x67, 0x2a, 0x07, 0x02, + 0x67, 0x2a, 0x07, 0x03, 0x67, 0x2a, 0x07, 0x04, 0x67, 0x2a, 0x07, 0x05, 0x67, 0x2a, 0x07, 0x06, + 0x67, 0x2a, 0x07, 0x07, 0x67, 0x2a, 0x07, 0x08, 0x67, 0x2a, 0x07, 0x09, 0x67, 0x2a, 0x07, 0x0a, + 0x67, 0x2a, 0x07, 0x0b, 0x67, 0x2a, 0x03, 0x00, 0x67, 0x2a, 0x03, 0x01, 0x67, 0x2a, 0x03, 0x02, + 0x67, 0x2a, 0x03, 0x03, 0x67, 0x2a, 0x03, 0x00, 0x00, 0x67, 0x2a, 0x03, 0x00, 0x01, 0x67, 0x2a, + 0x03, 0x02, 0x01, 0x67, 0x2a, 0x03, 0x02, 0x02, 0x67, 0x2a, 0x03, 0x03, 0x03, 0x67, 0x2a, 0x03, + 0x03, 0x04, 0x67, 0x2a, 0x03, 0x03, 0x05, 0x67, 0x2a, 0x03, 0x03, 0x03, 0x01, 0x67, 0x2a, 0x03, + 0x03, 0x04, 0x01, 0x67, 0x2a, 0x03, 0x03, 0x04, 0x02, 0x67, 0x2a, 0x03, 0x03, 0x05, 0x01, 0x67, + 0x2a, 0x03, 0x03, 0x05, 0x02, 0x67, 0x2a, 0x08, 0x01, 0x67, 0x2a, 0x08, 0x1e, 0x67, 0x2a, 0x08, + 0x22, 0x67, 0x2a, 0x08, 0x23, 0x67, 0x2a, 0x08, 0x04, 0x67, 0x2a, 0x08, 0x05, 0x67, 0x2a, 0x08, + 0xae, 0x7b, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x03, 0x0a, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x01, 0x06, 0x67, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, 0x02, 0x02, 0x2b, 0x06, + 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, 0x02, 0x03, 0x55, 0x04, 0x09, 0x55, 0x04, 0x11, 0x2b, 0x06, + 0x01, 0x05, 0x05, 0x07, 0x15, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x0e, 0x2b, 0x06, 0x01, + 0x05, 0x05, 0x07, 0x15, 0x00, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x15, 0x01, 0x55, 0x1d, 0x1e, + 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x15, 0x02, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, + 0x0b, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0c, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x01, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0e, 0x60, 0x86, 0x48, 0x01, + 0x65, 0x03, 0x04, 0x02, 0x01, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x60, 0x86, + 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, + 0x2b, 0x2b, 0x81, 0x04, 0x67, 0x2b, 0x67, 0x2b, 0x01, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x01, 0x02, + 0x03, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x01, 0x02, 0x03, 0x01, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x01, + 0x02, 0x03, 0x02, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x01, 0x02, 0x03, 0x03, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x01, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x02, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x03, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x04, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x05, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x06, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x08, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x09, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x0a, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x0b, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x0c, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x0d, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x0e, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x0f, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x10, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x11, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x12, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x00, 0x13, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x00, 0x14, 0x2b, 0x81, 0x04, 0x00, + 0x06, 0x2b, 0x81, 0x04, 0x00, 0x07, 0x2b, 0x81, 0x04, 0x00, 0x1c, 0x2b, 0x81, 0x04, 0x00, 0x1d, + 0x2b, 0x81, 0x04, 0x00, 0x09, 0x2b, 0x81, 0x04, 0x00, 0x08, 0x2b, 0x81, 0x04, 0x00, 0x1e, 0x2b, + 0x81, 0x04, 0x00, 0x1f, 0x2b, 0x81, 0x04, 0x00, 0x20, 0x2b, 0x81, 0x04, 0x00, 0x21, 0x2b, 0x81, + 0x04, 0x00, 0x0a, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x2b, 0x81, 0x04, 0x00, 0x23, 0x2b, 0x81, 0x04, + 0x00, 0x04, 0x2b, 0x81, 0x04, 0x00, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x16, 0x2b, 0x81, 0x04, 0x00, + 0x17, 0x2b, 0x81, 0x04, 0x00, 0x01, 0x2b, 0x81, 0x04, 0x00, 0x02, 0x2b, 0x81, 0x04, 0x00, 0x0f, + 0x2b, 0x81, 0x04, 0x00, 0x18, 0x2b, 0x81, 0x04, 0x00, 0x19, 0x2b, 0x81, 0x04, 0x00, 0x1a, 0x2b, + 0x81, 0x04, 0x00, 0x1b, 0x2b, 0x81, 0x04, 0x00, 0x03, 0x2b, 0x81, 0x04, 0x00, 0x10, 0x2b, 0x81, + 0x04, 0x00, 0x11, 0x2b, 0x81, 0x04, 0x00, 0x24, 0x2b, 0x81, 0x04, 0x00, 0x25, 0x2b, 0x81, 0x04, + 0x00, 0x26, 0x2b, 0x81, 0x04, 0x00, 0x27, 0x67, 0x2b, 0x01, 0x04, 0x01, 0x67, 0x2b, 0x01, 0x04, + 0x03, 0x67, 0x2b, 0x01, 0x04, 0x04, 0x67, 0x2b, 0x01, 0x04, 0x05, 0x67, 0x2b, 0x01, 0x04, 0x06, + 0x67, 0x2b, 0x01, 0x04, 0x07, 0x67, 0x2b, 0x01, 0x04, 0x08, 0x67, 0x2b, 0x01, 0x04, 0x09, 0x67, + 0x2b, 0x01, 0x04, 0x0a, 0x67, 0x2b, 0x01, 0x04, 0x0b, 0x67, 0x2b, 0x01, 0x04, 0x0c, 0x55, 0x1d, + 0x20, 0x00, 0x55, 0x1d, 0x21, 0x55, 0x1d, 0x36, 0x2a, 0x83, 0x08, 0x8c, 0x9a, 0x4b, 0x3d, 0x01, + 0x01, 0x01, 0x02, 0x2a, 0x83, 0x08, 0x8c, 0x9a, 0x4b, 0x3d, 0x01, 0x01, 0x01, 0x03, 0x2a, 0x83, + 0x08, 0x8c, 0x9a, 0x4b, 0x3d, 0x01, 0x01, 0x01, 0x04, 0x03, 0xa2, 0x31, 0x05, 0x03, 0x01, 0x09, + 0x01, 0x03, 0xa2, 0x31, 0x05, 0x03, 0x01, 0x09, 0x15, 0x03, 0xa2, 0x31, 0x05, 0x03, 0x01, 0x09, + 0x29, 0x03, 0xa2, 0x31, 0x05, 0x03, 0x01, 0x09, 0x04, 0x03, 0xa2, 0x31, 0x05, 0x03, 0x01, 0x09, + 0x18, 0x03, 0xa2, 0x31, 0x05, 0x03, 0x01, 0x09, 0x2c, 0x03, 0xa2, 0x31, 0x05, 0x03, 0x01, 0x09, + 0x03, 0x03, 0xa2, 0x31, 0x05, 0x03, 0x01, 0x09, 0x17, 0x03, 0xa2, 0x31, 0x05, 0x03, 0x01, 0x09, + 0x2b, 0x55, 0x1d, 0x09, 0x55, 0x1d, 0x1c, 0x55, 0x1d, 0x1d, 0x2a, 0x83, 0x1a, 0x8c, 0x9a, 0x44, + 0x2a, 0x83, 0x1a, 0x8c, 0x9a, 0x44, 0x01, 0x03, 0x2a, 0x83, 0x1a, 0x8c, 0x9a, 0x44, 0x01, 0x04, + 0x2a, 0x83, 0x1a, 0x8c, 0x9a, 0x44, 0x01, 0x06, 0x2a, 0x83, 0x1a, 0x8c, 0x9a, 0x44, 0x01, 0x05, + 0x2b, 0x06, 0x01, 0x05, 0x05, 0x08, 0x01, 0x01, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x08, 0x01, 0x02, + 0x2a, 0x86, 0x48, 0x86, 0xf6, 0x7d, 0x07, 0x42, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf6, 0x7d, 0x07, + 0x42, 0x1e, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x04, 0x10, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, + 0x30, 0x05, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x09, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x10, 0x01, 0x1b, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, + 0x05, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x19, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x01, 0x2d, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x02, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, + 0x03, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x01, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, + 0x02, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, + 0x04, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x06, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, + 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, + 0x0a, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x0b, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, + 0x03, 0x01, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x02, 0x28, 0xcf, 0x06, 0x03, 0x00, + 0x37, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x2a, 0x85, 0x03, 0x02, 0x09, 0x2a, 0x85, 0x03, 0x02, 0x02, + 0x03, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x04, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x09, 0x2a, 0x85, 0x03, + 0x02, 0x02, 0x0a, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x13, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x14, 0x2a, + 0x85, 0x03, 0x02, 0x02, 0x15, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x16, 0x2a, 0x85, 0x03, 0x02, 0x02, + 0x17, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x62, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x63, 0x2a, 0x85, 0x03, + 0x02, 0x02, 0x0e, 0x01, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x0e, 0x00, 0x2a, 0x85, 0x03, 0x02, 0x02, + 0x1e, 0x00, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1e, 0x01, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1f, 0x00, + 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1f, 0x01, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1f, 0x02, 0x2a, 0x85, + 0x03, 0x02, 0x02, 0x1f, 0x03, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1f, 0x04, 0x2a, 0x85, 0x03, 0x02, + 0x02, 0x1f, 0x05, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1f, 0x06, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1f, + 0x07, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x20, 0x00, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x20, 0x02, 0x2a, + 0x85, 0x03, 0x02, 0x02, 0x20, 0x03, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x20, 0x04, 0x2a, 0x85, 0x03, + 0x02, 0x02, 0x20, 0x05, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x21, 0x01, 0x2a, 0x85, 0x03, 0x02, 0x02, + 0x21, 0x02, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x21, 0x03, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x23, 0x00, + 0x2a, 0x85, 0x03, 0x02, 0x02, 0x23, 0x01, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x23, 0x02, 0x2a, 0x85, + 0x03, 0x02, 0x02, 0x23, 0x03, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x24, 0x00, 0x2a, 0x85, 0x03, 0x02, + 0x02, 0x24, 0x01, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x14, 0x01, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x14, + 0x02, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x14, 0x03, 0x2a, 0x85, 0x03, 0x02, 0x02, 0x14, 0x04, 0x2a, + 0x85, 0x03, 0x02, 0x09, 0x01, 0x06, 0x01, 0x2a, 0x85, 0x03, 0x02, 0x09, 0x01, 0x05, 0x03, 0x2a, + 0x85, 0x03, 0x02, 0x09, 0x01, 0x05, 0x04, 0x2a, 0x85, 0x03, 0x02, 0x09, 0x01, 0x03, 0x03, 0x2a, + 0x85, 0x03, 0x02, 0x09, 0x01, 0x03, 0x04, 0x2a, 0x85, 0x03, 0x02, 0x09, 0x01, 0x08, 0x01, 0x2b, + 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x11, 0x02, 0x55, 0x1d, 0x2e, 0x2b, 0x06, 0x01, 0x05, 0x05, + 0x07, 0x08, 0x03, 0x55, 0x04, 0x0e, 0x55, 0x04, 0x0f, 0x55, 0x04, 0x10, 0x55, 0x04, 0x12, 0x55, + 0x04, 0x13, 0x55, 0x04, 0x14, 0x55, 0x04, 0x15, 0x55, 0x04, 0x16, 0x55, 0x04, 0x17, 0x55, 0x04, + 0x18, 0x55, 0x04, 0x19, 0x55, 0x04, 0x1a, 0x55, 0x04, 0x1b, 0x55, 0x04, 0x1c, 0x55, 0x04, 0x1d, + 0x55, 0x04, 0x1e, 0x55, 0x04, 0x1f, 0x55, 0x04, 0x20, 0x55, 0x04, 0x21, 0x55, 0x04, 0x22, 0x55, + 0x04, 0x23, 0x55, 0x04, 0x24, 0x55, 0x04, 0x25, 0x55, 0x04, 0x26, 0x55, 0x04, 0x27, 0x55, 0x04, + 0x28, 0x55, 0x04, 0x2f, 0x55, 0x04, 0x30, 0x55, 0x04, 0x31, 0x55, 0x04, 0x32, 0x55, 0x04, 0x33, + 0x55, 0x04, 0x34, 0x55, 0x04, 0x35, 0x55, 0x04, 0x36, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x09, 0x10, 0x03, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x06, 0x60, 0x86, 0x48, + 0x01, 0x65, 0x03, 0x04, 0x01, 0x07, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x08, 0x60, + 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x1a, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, + 0x1b, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x1c, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x01, 0x2e, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2f, 0x60, 0x86, 0x48, 0x01, + 0x65, 0x03, 0x04, 0x01, 0x30, 0x2a, 0x83, 0x08, 0x8c, 0x9a, 0x4b, 0x3d, 0x01, 0x01, 0x03, 0x02, + 0x2a, 0x83, 0x08, 0x8c, 0x9a, 0x4b, 0x3d, 0x01, 0x01, 0x03, 0x03, 0x2a, 0x83, 0x08, 0x8c, 0x9a, + 0x4b, 0x3d, 0x01, 0x01, 0x03, 0x04, 0x55, 0x1d, 0x25, 0x00, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x01, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0a, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x01, 0x07, 0x2b, 0x24, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x2b, 0x24, + 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x01, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x02, + 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x03, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, + 0x01, 0x04, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x05, 0x2b, 0x24, 0x03, 0x03, 0x02, + 0x08, 0x01, 0x01, 0x06, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07, 0x2b, 0x24, 0x03, + 0x03, 0x02, 0x08, 0x01, 0x01, 0x08, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x09, 0x2b, + 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0a, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, + 0x0b, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0c, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, + 0x01, 0x01, 0x0d, 0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0e, 0x2a, 0x81, 0x7a, 0x01, + 0x81, 0x5f, 0x65, 0x82, 0x00, 0x01, 0x2a, 0x85, 0x03, 0x07, 0x01, 0x2a, 0x85, 0x03, 0x07, 0x01, + 0x01, 0x02, 0x02, 0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x02, 0x03, 0x2a, 0x85, 0x03, 0x07, 0x01, + 0x02, 0x01, 0x02, 0x01, 0x2a, 0x85, 0x03, 0x07, 0x01, 0x02, 0x01, 0x02, 0x02, 0x2a, 0x85, 0x03, + 0x07, 0x01, 0x02, 0x05, 0x01, 0x01, 0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x01, 0x01, 0x2a, 0x85, + 0x03, 0x07, 0x01, 0x01, 0x01, 0x02, 0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x03, 0x02, 0x2a, 0x85, + 0x03, 0x07, 0x01, 0x01, 0x03, 0x03, 0x2b, 0x65, 0x6e, 0x2b, 0x65, 0x6f, 0x2b, 0x65, 0x70, 0x2b, + 0x65, 0x71, 0x2b, 0x65, 0x72, 0x2b, 0x65, 0x73, +}; + +} // namespace + +extern "C" { + +const LibreSslAsn1Object LibreSslObjects_nid_no_patch[LibreSslObjectCount] = { + {"UNDEF", "undefined", 0, 0, nullptr, 0}, + {"rsadsi", "RSA Data Security, Inc.", 1, 6, objectData + 0, 0}, + {"pkcs", "RSA Data Security, Inc. PKCS", 2, 7, objectData + 6, 0}, + {"MD2", "md2", 3, 8, objectData + 13, 0}, + {"MD5", "md5", 4, 8, objectData + 21, 0}, + {"RC4", "rc4", 5, 8, objectData + 29, 0}, + {"rsaEncryption", "rsaEncryption", 6, 9, objectData + 37, 0}, + {"RSA-MD2", "md2WithRSAEncryption", 7, 9, objectData + 46, 0}, + {"RSA-MD5", "md5WithRSAEncryption", 8, 9, objectData + 55, 0}, + {"PBE-MD2-DES", "pbeWithMD2AndDES-CBC", 9, 9, objectData + 64, 0}, + {"PBE-MD5-DES", "pbeWithMD5AndDES-CBC", 10, 9, objectData + 73, 0}, + {"X500", "directory services (X.500)", 11, 1, objectData + 82, 0}, + {"X509", "X509", 12, 2, objectData + 83, 0}, + {"CN", "commonName", 13, 3, objectData + 85, 0}, + {"C", "countryName", 14, 3, objectData + 88, 0}, + {"L", "localityName", 15, 3, objectData + 91, 0}, + {"ST", "stateOrProvinceName", 16, 3, objectData + 94, 0}, + {"O", "organizationName", 17, 3, objectData + 97, 0}, + {"OU", "organizationalUnitName", 18, 3, objectData + 100, 0}, + {"RSA", "rsa", 19, 4, objectData + 103, 0}, + {"pkcs7", "pkcs7", 20, 8, objectData + 107, 0}, + {"pkcs7-data", "pkcs7-data", 21, 9, objectData + 115, 0}, + {"pkcs7-signedData", "pkcs7-signedData", 22, 9, objectData + 124, 0}, + {"pkcs7-envelopedData", "pkcs7-envelopedData", 23, 9, objectData + 133, 0}, + {"pkcs7-signedAndEnvelopedData", "pkcs7-signedAndEnvelopedData", 24, 9, objectData + 142, 0}, + {"pkcs7-digestData", "pkcs7-digestData", 25, 9, objectData + 151, 0}, + {"pkcs7-encryptedData", "pkcs7-encryptedData", 26, 9, objectData + 160, 0}, + {"pkcs3", "pkcs3", 27, 8, objectData + 169, 0}, + {"dhKeyAgreement", "dhKeyAgreement", 28, 9, objectData + 177, 0}, + {"DES-ECB", "des-ecb", 29, 5, objectData + 186, 0}, + {"DES-CFB", "des-cfb", 30, 5, objectData + 191, 0}, + {"DES-CBC", "des-cbc", 31, 5, objectData + 196, 0}, + {"DES-EDE", "des-ede", 32, 5, objectData + 201, 0}, + {"DES-EDE3", "des-ede3", 33, 0, nullptr, 0}, + {"IDEA-CBC", "idea-cbc", 34, 11, objectData + 206, 0}, + {"IDEA-CFB", "idea-cfb", 35, 0, nullptr, 0}, + {"IDEA-ECB", "idea-ecb", 36, 0, nullptr, 0}, + {"RC2-CBC", "rc2-cbc", 37, 8, objectData + 217, 0}, + {"RC2-ECB", "rc2-ecb", 38, 0, nullptr, 0}, + {"RC2-CFB", "rc2-cfb", 39, 0, nullptr, 0}, + {"RC2-OFB", "rc2-ofb", 40, 0, nullptr, 0}, + {"SHA", "sha", 41, 5, objectData + 225, 0}, + {"RSA-SHA", "shaWithRSAEncryption", 42, 5, objectData + 230, 0}, + {"DES-EDE-CBC", "des-ede-cbc", 43, 0, nullptr, 0}, + {"DES-EDE3-CBC", "des-ede3-cbc", 44, 8, objectData + 235, 0}, + {"DES-OFB", "des-ofb", 45, 5, objectData + 243, 0}, + {"IDEA-OFB", "idea-ofb", 46, 0, nullptr, 0}, + {"pkcs9", "pkcs9", 47, 8, objectData + 248, 0}, + {"emailAddress", "emailAddress", 48, 9, objectData + 256, 0}, + {"unstructuredName", "unstructuredName", 49, 9, objectData + 265, 0}, + {"contentType", "contentType", 50, 9, objectData + 274, 0}, + {"messageDigest", "messageDigest", 51, 9, objectData + 283, 0}, + {"signingTime", "signingTime", 52, 9, objectData + 292, 0}, + {"countersignature", "countersignature", 53, 9, objectData + 301, 0}, + {"challengePassword", "challengePassword", 54, 9, objectData + 310, 0}, + {"unstructuredAddress", "unstructuredAddress", 55, 9, objectData + 319, 0}, + {"extendedCertificateAttributes", "extendedCertificateAttributes", 56, 9, objectData + 328, 0}, + {"Netscape", "Netscape Communications Corp.", 57, 7, objectData + 337, 0}, + {"nsCertExt", "Netscape Certificate Extension", 58, 8, objectData + 344, 0}, + {"nsDataType", "Netscape Data Type", 59, 8, objectData + 352, 0}, + {"DES-EDE-CFB", "des-ede-cfb", 60, 0, nullptr, 0}, + {"DES-EDE3-CFB", "des-ede3-cfb", 61, 0, nullptr, 0}, + {"DES-EDE-OFB", "des-ede-ofb", 62, 0, nullptr, 0}, + {"DES-EDE3-OFB", "des-ede3-ofb", 63, 0, nullptr, 0}, + {"SHA1", "sha1", 64, 5, objectData + 360, 0}, + {"RSA-SHA1", "sha1WithRSAEncryption", 65, 9, objectData + 365, 0}, + {"DSA-SHA", "dsaWithSHA", 66, 5, objectData + 374, 0}, + {"DSA-old", "dsaEncryption-old", 67, 5, objectData + 379, 0}, + {"PBE-SHA1-RC2-64", "pbeWithSHA1AndRC2-CBC", 68, 9, objectData + 384, 0}, + {"PBKDF2", "PBKDF2", 69, 9, objectData + 393, 0}, + {"DSA-SHA1-old", "dsaWithSHA1-old", 70, 5, objectData + 402, 0}, + {"nsCertType", "Netscape Cert Type", 71, 9, objectData + 407, 0}, + {"nsBaseUrl", "Netscape Base Url", 72, 9, objectData + 416, 0}, + {"nsRevocationUrl", "Netscape Revocation Url", 73, 9, objectData + 425, 0}, + {"nsCaRevocationUrl", "Netscape CA Revocation Url", 74, 9, objectData + 434, 0}, + {"nsRenewalUrl", "Netscape Renewal Url", 75, 9, objectData + 443, 0}, + {"nsCaPolicyUrl", "Netscape CA Policy Url", 76, 9, objectData + 452, 0}, + {"nsSslServerName", "Netscape SSL Server Name", 77, 9, objectData + 461, 0}, + {"nsComment", "Netscape Comment", 78, 9, objectData + 470, 0}, + {"nsCertSequence", "Netscape Certificate Sequence", 79, 9, objectData + 479, 0}, + {"DESX-CBC", "desx-cbc", 80, 0, nullptr, 0}, + {"id-ce", "id-ce", 81, 2, objectData + 488, 0}, + {"subjectKeyIdentifier", "X509v3 Subject Key Identifier", 82, 3, objectData + 490, 0}, + {"keyUsage", "X509v3 Key Usage", 83, 3, objectData + 493, 0}, + {"privateKeyUsagePeriod", "X509v3 Private Key Usage Period", 84, 3, objectData + 496, 0}, + {"subjectAltName", "X509v3 Subject Alternative Name", 85, 3, objectData + 499, 0}, + {"issuerAltName", "X509v3 Issuer Alternative Name", 86, 3, objectData + 502, 0}, + {"basicConstraints", "X509v3 Basic Constraints", 87, 3, objectData + 505, 0}, + {"crlNumber", "X509v3 CRL Number", 88, 3, objectData + 508, 0}, + {"certificatePolicies", "X509v3 Certificate Policies", 89, 3, objectData + 511, 0}, + {"authorityKeyIdentifier", "X509v3 Authority Key Identifier", 90, 3, objectData + 514, 0}, + {"BF-CBC", "bf-cbc", 91, 9, objectData + 517, 0}, + {"BF-ECB", "bf-ecb", 92, 0, nullptr, 0}, + {"BF-CFB", "bf-cfb", 93, 0, nullptr, 0}, + {"BF-OFB", "bf-ofb", 94, 0, nullptr, 0}, + {"MDC2", "mdc2", 95, 4, objectData + 526, 0}, + {"RSA-MDC2", "mdc2WithRSA", 96, 4, objectData + 530, 0}, + {"RC4-40", "rc4-40", 97, 0, nullptr, 0}, + {"RC2-40-CBC", "rc2-40-cbc", 98, 0, nullptr, 0}, + {"GN", "givenName", 99, 3, objectData + 534, 0}, + {"SN", "surname", 100, 3, objectData + 537, 0}, + {"initials", "initials", 101, 3, objectData + 540, 0}, + {nullptr, nullptr, 0, 0, nullptr, 0}, + {"crlDistributionPoints", "X509v3 CRL Distribution Points", 103, 3, objectData + 543, 0}, + {"RSA-NP-MD5", "md5WithRSA", 104, 5, objectData + 546, 0}, + {"serialNumber", "serialNumber", 105, 3, objectData + 551, 0}, + {"title", "title", 106, 3, objectData + 554, 0}, + {"description", "description", 107, 3, objectData + 557, 0}, + {"CAST5-CBC", "cast5-cbc", 108, 9, objectData + 560, 0}, + {"CAST5-ECB", "cast5-ecb", 109, 0, nullptr, 0}, + {"CAST5-CFB", "cast5-cfb", 110, 0, nullptr, 0}, + {"CAST5-OFB", "cast5-ofb", 111, 0, nullptr, 0}, + {"pbeWithMD5AndCast5CBC", "pbeWithMD5AndCast5CBC", 112, 9, objectData + 569, 0}, + {"DSA-SHA1", "dsaWithSHA1", 113, 7, objectData + 578, 0}, + {"MD5-SHA1", "md5-sha1", 114, 0, nullptr, 0}, + {"RSA-SHA1-2", "sha1WithRSA", 115, 5, objectData + 585, 0}, + {"DSA", "dsaEncryption", 116, 7, objectData + 590, 0}, + {"RIPEMD160", "ripemd160", 117, 5, objectData + 597, 0}, + {nullptr, nullptr, 0, 0, nullptr, 0}, + {"RSA-RIPEMD160", "ripemd160WithRSA", 119, 6, objectData + 602, 0}, + {"RC5-CBC", "rc5-cbc", 120, 8, objectData + 608, 0}, + {"RC5-ECB", "rc5-ecb", 121, 0, nullptr, 0}, + {"RC5-CFB", "rc5-cfb", 122, 0, nullptr, 0}, + {"RC5-OFB", "rc5-ofb", 123, 0, nullptr, 0}, + {"RLE", "run length compression", 124, 6, objectData + 616, 0}, + {"ZLIB", "zlib compression", 125, 11, objectData + 622, 0}, + {"extendedKeyUsage", "X509v3 Extended Key Usage", 126, 3, objectData + 633, 0}, + {"PKIX", "PKIX", 127, 6, objectData + 636, 0}, + {"id-kp", "id-kp", 128, 7, objectData + 642, 0}, + {"serverAuth", "TLS Web Server Authentication", 129, 8, objectData + 649, 0}, + {"clientAuth", "TLS Web Client Authentication", 130, 8, objectData + 657, 0}, + {"codeSigning", "Code Signing", 131, 8, objectData + 665, 0}, + {"emailProtection", "E-mail Protection", 132, 8, objectData + 673, 0}, + {"timeStamping", "Time Stamping", 133, 8, objectData + 681, 0}, + {"msCodeInd", "Microsoft Individual Code Signing", 134, 10, objectData + 689, 0}, + {"msCodeCom", "Microsoft Commercial Code Signing", 135, 10, objectData + 699, 0}, + {"msCTLSign", "Microsoft Trust List Signing", 136, 10, objectData + 709, 0}, + {"msSGC", "Microsoft Server Gated Crypto", 137, 10, objectData + 719, 0}, + {"msEFS", "Microsoft Encrypted File System", 138, 10, objectData + 729, 0}, + {"nsSGC", "Netscape Server Gated Crypto", 139, 9, objectData + 739, 0}, + {"deltaCRL", "X509v3 Delta CRL Indicator", 140, 3, objectData + 748, 0}, + {"CRLReason", "X509v3 CRL Reason Code", 141, 3, objectData + 751, 0}, + {"invalidityDate", "Invalidity Date", 142, 3, objectData + 754, 0}, + {"SXNetID", "Strong Extranet ID", 143, 5, objectData + 757, 0}, + {"PBE-SHA1-RC4-128", "pbeWithSHA1And128BitRC4", 144, 10, objectData + 762, 0}, + {"PBE-SHA1-RC4-40", "pbeWithSHA1And40BitRC4", 145, 10, objectData + 772, 0}, + {"PBE-SHA1-3DES", "pbeWithSHA1And3-KeyTripleDES-CBC", 146, 10, objectData + 782, 0}, + {"PBE-SHA1-2DES", "pbeWithSHA1And2-KeyTripleDES-CBC", 147, 10, objectData + 792, 0}, + {"PBE-SHA1-RC2-128", "pbeWithSHA1And128BitRC2-CBC", 148, 10, objectData + 802, 0}, + {"PBE-SHA1-RC2-40", "pbeWithSHA1And40BitRC2-CBC", 149, 10, objectData + 812, 0}, + {"keyBag", "keyBag", 150, 11, objectData + 822, 0}, + {"pkcs8ShroudedKeyBag", "pkcs8ShroudedKeyBag", 151, 11, objectData + 833, 0}, + {"certBag", "certBag", 152, 11, objectData + 844, 0}, + {"crlBag", "crlBag", 153, 11, objectData + 855, 0}, + {"secretBag", "secretBag", 154, 11, objectData + 866, 0}, + {"safeContentsBag", "safeContentsBag", 155, 11, objectData + 877, 0}, + {"friendlyName", "friendlyName", 156, 9, objectData + 888, 0}, + {"localKeyID", "localKeyID", 157, 9, objectData + 897, 0}, + {"x509Certificate", "x509Certificate", 158, 10, objectData + 906, 0}, + {"sdsiCertificate", "sdsiCertificate", 159, 10, objectData + 916, 0}, + {"x509Crl", "x509Crl", 160, 10, objectData + 926, 0}, + {"PBES2", "PBES2", 161, 9, objectData + 936, 0}, + {"PBMAC1", "PBMAC1", 162, 9, objectData + 945, 0}, + {"hmacWithSHA1", "hmacWithSHA1", 163, 8, objectData + 954, 0}, + {"id-qt-cps", "Policy Qualifier CPS", 164, 8, objectData + 962, 0}, + {"id-qt-unotice", "Policy Qualifier User Notice", 165, 8, objectData + 970, 0}, + {"RC2-64-CBC", "rc2-64-cbc", 166, 0, nullptr, 0}, + {"SMIME-CAPS", "S/MIME Capabilities", 167, 9, objectData + 978, 0}, + {"PBE-MD2-RC2-64", "pbeWithMD2AndRC2-CBC", 168, 9, objectData + 987, 0}, + {"PBE-MD5-RC2-64", "pbeWithMD5AndRC2-CBC", 169, 9, objectData + 996, 0}, + {"PBE-SHA1-DES", "pbeWithSHA1AndDES-CBC", 170, 9, objectData + 1005, 0}, + {"msExtReq", "Microsoft Extension Request", 171, 10, objectData + 1014, 0}, + {"extReq", "Extension Request", 172, 9, objectData + 1024, 0}, + {"name", "name", 173, 3, objectData + 1033, 0}, + {"dnQualifier", "dnQualifier", 174, 3, objectData + 1036, 0}, + {"id-pe", "id-pe", 175, 7, objectData + 1039, 0}, + {"id-ad", "id-ad", 176, 7, objectData + 1046, 0}, + {"authorityInfoAccess", "Authority Information Access", 177, 8, objectData + 1053, 0}, + {"OCSP", "OCSP", 178, 8, objectData + 1061, 0}, + {"caIssuers", "CA Issuers", 179, 8, objectData + 1069, 0}, + {"OCSPSigning", "OCSP Signing", 180, 8, objectData + 1077, 0}, + {"ISO", "iso", 181, 0, nullptr, 0}, + {"member-body", "ISO Member Body", 182, 1, objectData + 1085, 0}, + {"ISO-US", "ISO US Member Body", 183, 3, objectData + 1086, 0}, + {"X9-57", "X9.57", 184, 5, objectData + 1089, 0}, + {"X9cm", "X9.57 CM ?", 185, 6, objectData + 1094, 0}, + {"pkcs1", "pkcs1", 186, 8, objectData + 1100, 0}, + {"pkcs5", "pkcs5", 187, 8, objectData + 1108, 0}, + {"SMIME", "S/MIME", 188, 9, objectData + 1116, 0}, + {"id-smime-mod", "id-smime-mod", 189, 10, objectData + 1125, 0}, + {"id-smime-ct", "id-smime-ct", 190, 10, objectData + 1135, 0}, + {"id-smime-aa", "id-smime-aa", 191, 10, objectData + 1145, 0}, + {"id-smime-alg", "id-smime-alg", 192, 10, objectData + 1155, 0}, + {"id-smime-cd", "id-smime-cd", 193, 10, objectData + 1165, 0}, + {"id-smime-spq", "id-smime-spq", 194, 10, objectData + 1175, 0}, + {"id-smime-cti", "id-smime-cti", 195, 10, objectData + 1185, 0}, + {"id-smime-mod-cms", "id-smime-mod-cms", 196, 11, objectData + 1195, 0}, + {"id-smime-mod-ess", "id-smime-mod-ess", 197, 11, objectData + 1206, 0}, + {"id-smime-mod-oid", "id-smime-mod-oid", 198, 11, objectData + 1217, 0}, + {"id-smime-mod-msg-v3", "id-smime-mod-msg-v3", 199, 11, objectData + 1228, 0}, + {"id-smime-mod-ets-eSignature-88", "id-smime-mod-ets-eSignature-88", 200, 11, objectData + 1239, 0}, + {"id-smime-mod-ets-eSignature-97", "id-smime-mod-ets-eSignature-97", 201, 11, objectData + 1250, 0}, + {"id-smime-mod-ets-eSigPolicy-88", "id-smime-mod-ets-eSigPolicy-88", 202, 11, objectData + 1261, 0}, + {"id-smime-mod-ets-eSigPolicy-97", "id-smime-mod-ets-eSigPolicy-97", 203, 11, objectData + 1272, 0}, + {"id-smime-ct-receipt", "id-smime-ct-receipt", 204, 11, objectData + 1283, 0}, + {"id-smime-ct-authData", "id-smime-ct-authData", 205, 11, objectData + 1294, 0}, + {"id-smime-ct-publishCert", "id-smime-ct-publishCert", 206, 11, objectData + 1305, 0}, + {"id-smime-ct-TSTInfo", "id-smime-ct-TSTInfo", 207, 11, objectData + 1316, 0}, + {"id-smime-ct-TDTInfo", "id-smime-ct-TDTInfo", 208, 11, objectData + 1327, 0}, + {"id-smime-ct-contentInfo", "id-smime-ct-contentInfo", 209, 11, objectData + 1338, 0}, + {"id-smime-ct-DVCSRequestData", "id-smime-ct-DVCSRequestData", 210, 11, objectData + 1349, 0}, + {"id-smime-ct-DVCSResponseData", "id-smime-ct-DVCSResponseData", 211, 11, objectData + 1360, 0}, + {"id-smime-aa-receiptRequest", "id-smime-aa-receiptRequest", 212, 11, objectData + 1371, 0}, + {"id-smime-aa-securityLabel", "id-smime-aa-securityLabel", 213, 11, objectData + 1382, 0}, + {"id-smime-aa-mlExpandHistory", "id-smime-aa-mlExpandHistory", 214, 11, objectData + 1393, 0}, + {"id-smime-aa-contentHint", "id-smime-aa-contentHint", 215, 11, objectData + 1404, 0}, + {"id-smime-aa-msgSigDigest", "id-smime-aa-msgSigDigest", 216, 11, objectData + 1415, 0}, + {"id-smime-aa-encapContentType", "id-smime-aa-encapContentType", 217, 11, objectData + 1426, 0}, + {"id-smime-aa-contentIdentifier", "id-smime-aa-contentIdentifier", 218, 11, objectData + 1437, 0}, + {"id-smime-aa-macValue", "id-smime-aa-macValue", 219, 11, objectData + 1448, 0}, + {"id-smime-aa-equivalentLabels", "id-smime-aa-equivalentLabels", 220, 11, objectData + 1459, 0}, + {"id-smime-aa-contentReference", "id-smime-aa-contentReference", 221, 11, objectData + 1470, 0}, + {"id-smime-aa-encrypKeyPref", "id-smime-aa-encrypKeyPref", 222, 11, objectData + 1481, 0}, + {"id-smime-aa-signingCertificate", "id-smime-aa-signingCertificate", 223, 11, objectData + 1492, 0}, + {"id-smime-aa-smimeEncryptCerts", "id-smime-aa-smimeEncryptCerts", 224, 11, objectData + 1503, 0}, + {"id-smime-aa-timeStampToken", "id-smime-aa-timeStampToken", 225, 11, objectData + 1514, 0}, + {"id-smime-aa-ets-sigPolicyId", "id-smime-aa-ets-sigPolicyId", 226, 11, objectData + 1525, 0}, + {"id-smime-aa-ets-commitmentType", "id-smime-aa-ets-commitmentType", 227, 11, objectData + 1536, 0}, + {"id-smime-aa-ets-signerLocation", "id-smime-aa-ets-signerLocation", 228, 11, objectData + 1547, 0}, + {"id-smime-aa-ets-signerAttr", "id-smime-aa-ets-signerAttr", 229, 11, objectData + 1558, 0}, + {"id-smime-aa-ets-otherSigCert", "id-smime-aa-ets-otherSigCert", 230, 11, objectData + 1569, 0}, + {"id-smime-aa-ets-contentTimestamp", "id-smime-aa-ets-contentTimestamp", 231, 11, objectData + 1580, 0}, + {"id-smime-aa-ets-CertificateRefs", "id-smime-aa-ets-CertificateRefs", 232, 11, objectData + 1591, 0}, + {"id-smime-aa-ets-RevocationRefs", "id-smime-aa-ets-RevocationRefs", 233, 11, objectData + 1602, 0}, + {"id-smime-aa-ets-certValues", "id-smime-aa-ets-certValues", 234, 11, objectData + 1613, 0}, + {"id-smime-aa-ets-revocationValues", "id-smime-aa-ets-revocationValues", 235, 11, objectData + 1624, 0}, + {"id-smime-aa-ets-escTimeStamp", "id-smime-aa-ets-escTimeStamp", 236, 11, objectData + 1635, 0}, + {"id-smime-aa-ets-certCRLTimestamp", "id-smime-aa-ets-certCRLTimestamp", 237, 11, objectData + 1646, 0}, + {"id-smime-aa-ets-archiveTimeStamp", "id-smime-aa-ets-archiveTimeStamp", 238, 11, objectData + 1657, 0}, + {"id-smime-aa-signatureType", "id-smime-aa-signatureType", 239, 11, objectData + 1668, 0}, + {"id-smime-aa-dvcs-dvc", "id-smime-aa-dvcs-dvc", 240, 11, objectData + 1679, 0}, + {"id-smime-alg-ESDHwith3DES", "id-smime-alg-ESDHwith3DES", 241, 11, objectData + 1690, 0}, + {"id-smime-alg-ESDHwithRC2", "id-smime-alg-ESDHwithRC2", 242, 11, objectData + 1701, 0}, + {"id-smime-alg-3DESwrap", "id-smime-alg-3DESwrap", 243, 11, objectData + 1712, 0}, + {"id-smime-alg-RC2wrap", "id-smime-alg-RC2wrap", 244, 11, objectData + 1723, 0}, + {"id-smime-alg-ESDH", "id-smime-alg-ESDH", 245, 11, objectData + 1734, 0}, + {"id-smime-alg-CMS3DESwrap", "id-smime-alg-CMS3DESwrap", 246, 11, objectData + 1745, 0}, + {"id-smime-alg-CMSRC2wrap", "id-smime-alg-CMSRC2wrap", 247, 11, objectData + 1756, 0}, + {"id-smime-cd-ldap", "id-smime-cd-ldap", 248, 11, objectData + 1767, 0}, + {"id-smime-spq-ets-sqt-uri", "id-smime-spq-ets-sqt-uri", 249, 11, objectData + 1778, 0}, + {"id-smime-spq-ets-sqt-unotice", "id-smime-spq-ets-sqt-unotice", 250, 11, objectData + 1789, 0}, + {"id-smime-cti-ets-proofOfOrigin", "id-smime-cti-ets-proofOfOrigin", 251, 11, objectData + 1800, 0}, + {"id-smime-cti-ets-proofOfReceipt", "id-smime-cti-ets-proofOfReceipt", 252, 11, objectData + 1811, 0}, + {"id-smime-cti-ets-proofOfDelivery", "id-smime-cti-ets-proofOfDelivery", 253, 11, objectData + 1822, 0}, + {"id-smime-cti-ets-proofOfSender", "id-smime-cti-ets-proofOfSender", 254, 11, objectData + 1833, 0}, + {"id-smime-cti-ets-proofOfApproval", "id-smime-cti-ets-proofOfApproval", 255, 11, objectData + 1844, 0}, + {"id-smime-cti-ets-proofOfCreation", "id-smime-cti-ets-proofOfCreation", 256, 11, objectData + 1855, 0}, + {"MD4", "md4", 257, 8, objectData + 1866, 0}, + {"id-pkix-mod", "id-pkix-mod", 258, 7, objectData + 1874, 0}, + {"id-qt", "id-qt", 259, 7, objectData + 1881, 0}, + {"id-it", "id-it", 260, 7, objectData + 1888, 0}, + {"id-pkip", "id-pkip", 261, 7, objectData + 1895, 0}, + {"id-alg", "id-alg", 262, 7, objectData + 1902, 0}, + {"id-cmc", "id-cmc", 263, 7, objectData + 1909, 0}, + {"id-on", "id-on", 264, 7, objectData + 1916, 0}, + {"id-pda", "id-pda", 265, 7, objectData + 1923, 0}, + {"id-aca", "id-aca", 266, 7, objectData + 1930, 0}, + {"id-qcs", "id-qcs", 267, 7, objectData + 1937, 0}, + {"id-cct", "id-cct", 268, 7, objectData + 1944, 0}, + {"id-pkix1-explicit-88", "id-pkix1-explicit-88", 269, 8, objectData + 1951, 0}, + {"id-pkix1-implicit-88", "id-pkix1-implicit-88", 270, 8, objectData + 1959, 0}, + {"id-pkix1-explicit-93", "id-pkix1-explicit-93", 271, 8, objectData + 1967, 0}, + {"id-pkix1-implicit-93", "id-pkix1-implicit-93", 272, 8, objectData + 1975, 0}, + {"id-mod-crmf", "id-mod-crmf", 273, 8, objectData + 1983, 0}, + {"id-mod-cmc", "id-mod-cmc", 274, 8, objectData + 1991, 0}, + {"id-mod-kea-profile-88", "id-mod-kea-profile-88", 275, 8, objectData + 1999, 0}, + {"id-mod-kea-profile-93", "id-mod-kea-profile-93", 276, 8, objectData + 2007, 0}, + {"id-mod-cmp", "id-mod-cmp", 277, 8, objectData + 2015, 0}, + {"id-mod-qualified-cert-88", "id-mod-qualified-cert-88", 278, 8, objectData + 2023, 0}, + {"id-mod-qualified-cert-93", "id-mod-qualified-cert-93", 279, 8, objectData + 2031, 0}, + {"id-mod-attribute-cert", "id-mod-attribute-cert", 280, 8, objectData + 2039, 0}, + {"id-mod-timestamp-protocol", "id-mod-timestamp-protocol", 281, 8, objectData + 2047, 0}, + {"id-mod-ocsp", "id-mod-ocsp", 282, 8, objectData + 2055, 0}, + {"id-mod-dvcs", "id-mod-dvcs", 283, 8, objectData + 2063, 0}, + {"id-mod-cmp2000", "id-mod-cmp2000", 284, 8, objectData + 2071, 0}, + {"biometricInfo", "Biometric Info", 285, 8, objectData + 2079, 0}, + {"qcStatements", "qcStatements", 286, 8, objectData + 2087, 0}, + {"ac-auditEntity", "ac-auditEntity", 287, 8, objectData + 2095, 0}, + {"ac-targeting", "ac-targeting", 288, 8, objectData + 2103, 0}, + {"aaControls", "aaControls", 289, 8, objectData + 2111, 0}, + {"sbgp-ipAddrBlock", "sbgp-ipAddrBlock", 290, 8, objectData + 2119, 0}, + {"sbgp-autonomousSysNum", "sbgp-autonomousSysNum", 291, 8, objectData + 2127, 0}, + {"sbgp-routerIdentifier", "sbgp-routerIdentifier", 292, 8, objectData + 2135, 0}, + {"textNotice", "textNotice", 293, 8, objectData + 2143, 0}, + {"ipsecEndSystem", "IPSec End System", 294, 8, objectData + 2151, 0}, + {"ipsecTunnel", "IPSec Tunnel", 295, 8, objectData + 2159, 0}, + {"ipsecUser", "IPSec User", 296, 8, objectData + 2167, 0}, + {"DVCS", "dvcs", 297, 8, objectData + 2175, 0}, + {"id-it-caProtEncCert", "id-it-caProtEncCert", 298, 8, objectData + 2183, 0}, + {"id-it-signKeyPairTypes", "id-it-signKeyPairTypes", 299, 8, objectData + 2191, 0}, + {"id-it-encKeyPairTypes", "id-it-encKeyPairTypes", 300, 8, objectData + 2199, 0}, + {"id-it-preferredSymmAlg", "id-it-preferredSymmAlg", 301, 8, objectData + 2207, 0}, + {"id-it-caKeyUpdateInfo", "id-it-caKeyUpdateInfo", 302, 8, objectData + 2215, 0}, + {"id-it-currentCRL", "id-it-currentCRL", 303, 8, objectData + 2223, 0}, + {"id-it-unsupportedOIDs", "id-it-unsupportedOIDs", 304, 8, objectData + 2231, 0}, + {"id-it-subscriptionRequest", "id-it-subscriptionRequest", 305, 8, objectData + 2239, 0}, + {"id-it-subscriptionResponse", "id-it-subscriptionResponse", 306, 8, objectData + 2247, 0}, + {"id-it-keyPairParamReq", "id-it-keyPairParamReq", 307, 8, objectData + 2255, 0}, + {"id-it-keyPairParamRep", "id-it-keyPairParamRep", 308, 8, objectData + 2263, 0}, + {"id-it-revPassphrase", "id-it-revPassphrase", 309, 8, objectData + 2271, 0}, + {"id-it-implicitConfirm", "id-it-implicitConfirm", 310, 8, objectData + 2279, 0}, + {"id-it-confirmWaitTime", "id-it-confirmWaitTime", 311, 8, objectData + 2287, 0}, + {"id-it-origPKIMessage", "id-it-origPKIMessage", 312, 8, objectData + 2295, 0}, + {"id-regCtrl", "id-regCtrl", 313, 8, objectData + 2303, 0}, + {"id-regInfo", "id-regInfo", 314, 8, objectData + 2311, 0}, + {"id-regCtrl-regToken", "id-regCtrl-regToken", 315, 9, objectData + 2319, 0}, + {"id-regCtrl-authenticator", "id-regCtrl-authenticator", 316, 9, objectData + 2328, 0}, + {"id-regCtrl-pkiPublicationInfo", "id-regCtrl-pkiPublicationInfo", 317, 9, objectData + 2337, 0}, + {"id-regCtrl-pkiArchiveOptions", "id-regCtrl-pkiArchiveOptions", 318, 9, objectData + 2346, 0}, + {"id-regCtrl-oldCertID", "id-regCtrl-oldCertID", 319, 9, objectData + 2355, 0}, + {"id-regCtrl-protocolEncrKey", "id-regCtrl-protocolEncrKey", 320, 9, objectData + 2364, 0}, + {"id-regInfo-utf8Pairs", "id-regInfo-utf8Pairs", 321, 9, objectData + 2373, 0}, + {"id-regInfo-certReq", "id-regInfo-certReq", 322, 9, objectData + 2382, 0}, + {"id-alg-des40", "id-alg-des40", 323, 8, objectData + 2391, 0}, + {"id-alg-noSignature", "id-alg-noSignature", 324, 8, objectData + 2399, 0}, + {"id-alg-dh-sig-hmac-sha1", "id-alg-dh-sig-hmac-sha1", 325, 8, objectData + 2407, 0}, + {"id-alg-dh-pop", "id-alg-dh-pop", 326, 8, objectData + 2415, 0}, + {"id-cmc-statusInfo", "id-cmc-statusInfo", 327, 8, objectData + 2423, 0}, + {"id-cmc-identification", "id-cmc-identification", 328, 8, objectData + 2431, 0}, + {"id-cmc-identityProof", "id-cmc-identityProof", 329, 8, objectData + 2439, 0}, + {"id-cmc-dataReturn", "id-cmc-dataReturn", 330, 8, objectData + 2447, 0}, + {"id-cmc-transactionId", "id-cmc-transactionId", 331, 8, objectData + 2455, 0}, + {"id-cmc-senderNonce", "id-cmc-senderNonce", 332, 8, objectData + 2463, 0}, + {"id-cmc-recipientNonce", "id-cmc-recipientNonce", 333, 8, objectData + 2471, 0}, + {"id-cmc-addExtensions", "id-cmc-addExtensions", 334, 8, objectData + 2479, 0}, + {"id-cmc-encryptedPOP", "id-cmc-encryptedPOP", 335, 8, objectData + 2487, 0}, + {"id-cmc-decryptedPOP", "id-cmc-decryptedPOP", 336, 8, objectData + 2495, 0}, + {"id-cmc-lraPOPWitness", "id-cmc-lraPOPWitness", 337, 8, objectData + 2503, 0}, + {"id-cmc-getCert", "id-cmc-getCert", 338, 8, objectData + 2511, 0}, + {"id-cmc-getCRL", "id-cmc-getCRL", 339, 8, objectData + 2519, 0}, + {"id-cmc-revokeRequest", "id-cmc-revokeRequest", 340, 8, objectData + 2527, 0}, + {"id-cmc-regInfo", "id-cmc-regInfo", 341, 8, objectData + 2535, 0}, + {"id-cmc-responseInfo", "id-cmc-responseInfo", 342, 8, objectData + 2543, 0}, + {"id-cmc-queryPending", "id-cmc-queryPending", 343, 8, objectData + 2551, 0}, + {"id-cmc-popLinkRandom", "id-cmc-popLinkRandom", 344, 8, objectData + 2559, 0}, + {"id-cmc-popLinkWitness", "id-cmc-popLinkWitness", 345, 8, objectData + 2567, 0}, + {"id-cmc-confirmCertAcceptance", "id-cmc-confirmCertAcceptance", 346, 8, objectData + 2575, 0}, + {"id-on-personalData", "id-on-personalData", 347, 8, objectData + 2583, 0}, + {"id-pda-dateOfBirth", "id-pda-dateOfBirth", 348, 8, objectData + 2591, 0}, + {"id-pda-placeOfBirth", "id-pda-placeOfBirth", 349, 8, objectData + 2599, 0}, + {nullptr, nullptr, 0, 0, nullptr, 0}, + {"id-pda-gender", "id-pda-gender", 351, 8, objectData + 2607, 0}, + {"id-pda-countryOfCitizenship", "id-pda-countryOfCitizenship", 352, 8, objectData + 2615, 0}, + {"id-pda-countryOfResidence", "id-pda-countryOfResidence", 353, 8, objectData + 2623, 0}, + {"id-aca-authenticationInfo", "id-aca-authenticationInfo", 354, 8, objectData + 2631, 0}, + {"id-aca-accessIdentity", "id-aca-accessIdentity", 355, 8, objectData + 2639, 0}, + {"id-aca-chargingIdentity", "id-aca-chargingIdentity", 356, 8, objectData + 2647, 0}, + {"id-aca-group", "id-aca-group", 357, 8, objectData + 2655, 0}, + {"id-aca-role", "id-aca-role", 358, 8, objectData + 2663, 0}, + {"id-qcs-pkixQCSyntax-v1", "id-qcs-pkixQCSyntax-v1", 359, 8, objectData + 2671, 0}, + {"id-cct-crs", "id-cct-crs", 360, 8, objectData + 2679, 0}, + {"id-cct-PKIData", "id-cct-PKIData", 361, 8, objectData + 2687, 0}, + {"id-cct-PKIResponse", "id-cct-PKIResponse", 362, 8, objectData + 2695, 0}, + {"ad_timestamping", "AD Time Stamping", 363, 8, objectData + 2703, 0}, + {"AD_DVCS", "ad dvcs", 364, 8, objectData + 2711, 0}, + {"basicOCSPResponse", "Basic OCSP Response", 365, 9, objectData + 2719, 0}, + {"Nonce", "OCSP Nonce", 366, 9, objectData + 2728, 0}, + {"CrlID", "OCSP CRL ID", 367, 9, objectData + 2737, 0}, + {"acceptableResponses", "Acceptable OCSP Responses", 368, 9, objectData + 2746, 0}, + {"noCheck", "OCSP No Check", 369, 9, objectData + 2755, 0}, + {"archiveCutoff", "OCSP Archive Cutoff", 370, 9, objectData + 2764, 0}, + {"serviceLocator", "OCSP Service Locator", 371, 9, objectData + 2773, 0}, + {"extendedStatus", "Extended OCSP Status", 372, 9, objectData + 2782, 0}, + {"valid", "valid", 373, 9, objectData + 2791, 0}, + {"path", "path", 374, 9, objectData + 2800, 0}, + {"trustRoot", "Trust Root", 375, 9, objectData + 2809, 0}, + {"algorithm", "algorithm", 376, 4, objectData + 2818, 0}, + {"rsaSignature", "rsaSignature", 377, 5, objectData + 2822, 0}, + {"X500algorithms", "directory services - algorithms", 378, 2, objectData + 2827, 0}, + {"ORG", "org", 379, 1, objectData + 2829, 0}, + {"DOD", "dod", 380, 2, objectData + 2830, 0}, + {"IANA", "iana", 381, 3, objectData + 2832, 0}, + {"directory", "Directory", 382, 4, objectData + 2835, 0}, + {"mgmt", "Management", 383, 4, objectData + 2839, 0}, + {"experimental", "Experimental", 384, 4, objectData + 2843, 0}, + {"private", "Private", 385, 4, objectData + 2847, 0}, + {"security", "Security", 386, 4, objectData + 2851, 0}, + {"snmpv2", "SNMPv2", 387, 4, objectData + 2855, 0}, + {"Mail", "Mail", 388, 4, objectData + 2859, 0}, + {"enterprises", "Enterprises", 389, 5, objectData + 2863, 0}, + {"dcobject", "dcObject", 390, 9, objectData + 2868, 0}, + {"DC", "domainComponent", 391, 10, objectData + 2877, 0}, + {"domain", "Domain", 392, 10, objectData + 2887, 0}, + {"NULL", "NULL", 393, 0, nullptr, 0}, + {"selected-attribute-types", "Selected Attribute Types", 394, 3, objectData + 2897, 0}, + {"clearance", "clearance", 395, 4, objectData + 2900, 0}, + {"RSA-MD4", "md4WithRSAEncryption", 396, 9, objectData + 2904, 0}, + {"ac-proxying", "ac-proxying", 397, 8, objectData + 2913, 0}, + {"subjectInfoAccess", "Subject Information Access", 398, 8, objectData + 2921, 0}, + {"id-aca-encAttrs", "id-aca-encAttrs", 399, 8, objectData + 2929, 0}, + {"role", "role", 400, 3, objectData + 2937, 0}, + {"policyConstraints", "X509v3 Policy Constraints", 401, 3, objectData + 2940, 0}, + {"targetInformation", "X509v3 AC Targeting", 402, 3, objectData + 2943, 0}, + {"noRevAvail", "X509v3 No Revocation Available", 403, 3, objectData + 2946, 0}, + {"NULL", "NULL", 404, 0, nullptr, 0}, + {"ansi-X9-62", "ANSI X9.62", 405, 5, objectData + 2949, 0}, + {"prime-field", "prime-field", 406, 7, objectData + 2954, 0}, + {"characteristic-two-field", "characteristic-two-field", 407, 7, objectData + 2961, 0}, + {"id-ecPublicKey", "id-ecPublicKey", 408, 7, objectData + 2968, 0}, + {"prime192v1", "prime192v1", 409, 8, objectData + 2975, 0}, + {"prime192v2", "prime192v2", 410, 8, objectData + 2983, 0}, + {"prime192v3", "prime192v3", 411, 8, objectData + 2991, 0}, + {"prime239v1", "prime239v1", 412, 8, objectData + 2999, 0}, + {"prime239v2", "prime239v2", 413, 8, objectData + 3007, 0}, + {"prime239v3", "prime239v3", 414, 8, objectData + 3015, 0}, + {"prime256v1", "prime256v1", 415, 8, objectData + 3023, 0}, + {"ecdsa-with-SHA1", "ecdsa-with-SHA1", 416, 7, objectData + 3031, 0}, + {"CSPName", "Microsoft CSP Name", 417, 9, objectData + 3038, 0}, + {"AES-128-ECB", "aes-128-ecb", 418, 9, objectData + 3047, 0}, + {"AES-128-CBC", "aes-128-cbc", 419, 9, objectData + 3056, 0}, + {"AES-128-OFB", "aes-128-ofb", 420, 9, objectData + 3065, 0}, + {"AES-128-CFB", "aes-128-cfb", 421, 9, objectData + 3074, 0}, + {"AES-192-ECB", "aes-192-ecb", 422, 9, objectData + 3083, 0}, + {"AES-192-CBC", "aes-192-cbc", 423, 9, objectData + 3092, 0}, + {"AES-192-OFB", "aes-192-ofb", 424, 9, objectData + 3101, 0}, + {"AES-192-CFB", "aes-192-cfb", 425, 9, objectData + 3110, 0}, + {"AES-256-ECB", "aes-256-ecb", 426, 9, objectData + 3119, 0}, + {"AES-256-CBC", "aes-256-cbc", 427, 9, objectData + 3128, 0}, + {"AES-256-OFB", "aes-256-ofb", 428, 9, objectData + 3137, 0}, + {"AES-256-CFB", "aes-256-cfb", 429, 9, objectData + 3146, 0}, + {"holdInstructionCode", "Hold Instruction Code", 430, 3, objectData + 3155, 0}, + {"holdInstructionNone", "Hold Instruction None", 431, 7, objectData + 3158, 0}, + {"holdInstructionCallIssuer", "Hold Instruction Call Issuer", 432, 7, objectData + 3165, 0}, + {"holdInstructionReject", "Hold Instruction Reject", 433, 7, objectData + 3172, 0}, + {"data", "data", 434, 1, objectData + 3179, 0}, + {"pss", "pss", 435, 3, objectData + 3180, 0}, + {"ucl", "ucl", 436, 7, objectData + 3183, 0}, + {"pilot", "pilot", 437, 8, objectData + 3190, 0}, + {"pilotAttributeType", "pilotAttributeType", 438, 9, objectData + 3198, 0}, + {"pilotAttributeSyntax", "pilotAttributeSyntax", 439, 9, objectData + 3207, 0}, + {"pilotObjectClass", "pilotObjectClass", 440, 9, objectData + 3216, 0}, + {"pilotGroups", "pilotGroups", 441, 9, objectData + 3225, 0}, + {"iA5StringSyntax", "iA5StringSyntax", 442, 10, objectData + 3234, 0}, + {"caseIgnoreIA5StringSyntax", "caseIgnoreIA5StringSyntax", 443, 10, objectData + 3244, 0}, + {"pilotObject", "pilotObject", 444, 10, objectData + 3254, 0}, + {"pilotPerson", "pilotPerson", 445, 10, objectData + 3264, 0}, + {"account", "account", 446, 10, objectData + 3274, 0}, + {"document", "document", 447, 10, objectData + 3284, 0}, + {"room", "room", 448, 10, objectData + 3294, 0}, + {"documentSeries", "documentSeries", 449, 10, objectData + 3304, 0}, + {"rFC822localPart", "rFC822localPart", 450, 10, objectData + 3314, 0}, + {"dNSDomain", "dNSDomain", 451, 10, objectData + 3324, 0}, + {"domainRelatedObject", "domainRelatedObject", 452, 10, objectData + 3334, 0}, + {"friendlyCountry", "friendlyCountry", 453, 10, objectData + 3344, 0}, + {"simpleSecurityObject", "simpleSecurityObject", 454, 10, objectData + 3354, 0}, + {"pilotOrganization", "pilotOrganization", 455, 10, objectData + 3364, 0}, + {"pilotDSA", "pilotDSA", 456, 10, objectData + 3374, 0}, + {"qualityLabelledData", "qualityLabelledData", 457, 10, objectData + 3384, 0}, + {"UID", "userId", 458, 10, objectData + 3394, 0}, + {"textEncodedORAddress", "textEncodedORAddress", 459, 10, objectData + 3404, 0}, + {"mail", "rfc822Mailbox", 460, 10, objectData + 3414, 0}, + {"info", "info", 461, 10, objectData + 3424, 0}, + {"favouriteDrink", "favouriteDrink", 462, 10, objectData + 3434, 0}, + {"roomNumber", "roomNumber", 463, 10, objectData + 3444, 0}, + {"photo", "photo", 464, 10, objectData + 3454, 0}, + {"userClass", "userClass", 465, 10, objectData + 3464, 0}, + {"host", "host", 466, 10, objectData + 3474, 0}, + {"manager", "manager", 467, 10, objectData + 3484, 0}, + {"documentIdentifier", "documentIdentifier", 468, 10, objectData + 3494, 0}, + {"documentTitle", "documentTitle", 469, 10, objectData + 3504, 0}, + {"documentVersion", "documentVersion", 470, 10, objectData + 3514, 0}, + {"documentAuthor", "documentAuthor", 471, 10, objectData + 3524, 0}, + {"documentLocation", "documentLocation", 472, 10, objectData + 3534, 0}, + {"homeTelephoneNumber", "homeTelephoneNumber", 473, 10, objectData + 3544, 0}, + {"secretary", "secretary", 474, 10, objectData + 3554, 0}, + {"otherMailbox", "otherMailbox", 475, 10, objectData + 3564, 0}, + {"lastModifiedTime", "lastModifiedTime", 476, 10, objectData + 3574, 0}, + {"lastModifiedBy", "lastModifiedBy", 477, 10, objectData + 3584, 0}, + {"aRecord", "aRecord", 478, 10, objectData + 3594, 0}, + {"pilotAttributeType27", "pilotAttributeType27", 479, 10, objectData + 3604, 0}, + {"mXRecord", "mXRecord", 480, 10, objectData + 3614, 0}, + {"nSRecord", "nSRecord", 481, 10, objectData + 3624, 0}, + {"sOARecord", "sOARecord", 482, 10, objectData + 3634, 0}, + {"cNAMERecord", "cNAMERecord", 483, 10, objectData + 3644, 0}, + {"associatedDomain", "associatedDomain", 484, 10, objectData + 3654, 0}, + {"associatedName", "associatedName", 485, 10, objectData + 3664, 0}, + {"homePostalAddress", "homePostalAddress", 486, 10, objectData + 3674, 0}, + {"personalTitle", "personalTitle", 487, 10, objectData + 3684, 0}, + {"mobileTelephoneNumber", "mobileTelephoneNumber", 488, 10, objectData + 3694, 0}, + {"pagerTelephoneNumber", "pagerTelephoneNumber", 489, 10, objectData + 3704, 0}, + {"friendlyCountryName", "friendlyCountryName", 490, 10, objectData + 3714, 0}, + {"organizationalStatus", "organizationalStatus", 491, 10, objectData + 3724, 0}, + {"janetMailbox", "janetMailbox", 492, 10, objectData + 3734, 0}, + {"mailPreferenceOption", "mailPreferenceOption", 493, 10, objectData + 3744, 0}, + {"buildingName", "buildingName", 494, 10, objectData + 3754, 0}, + {"dSAQuality", "dSAQuality", 495, 10, objectData + 3764, 0}, + {"singleLevelQuality", "singleLevelQuality", 496, 10, objectData + 3774, 0}, + {"subtreeMinimumQuality", "subtreeMinimumQuality", 497, 10, objectData + 3784, 0}, + {"subtreeMaximumQuality", "subtreeMaximumQuality", 498, 10, objectData + 3794, 0}, + {"personalSignature", "personalSignature", 499, 10, objectData + 3804, 0}, + {"dITRedirect", "dITRedirect", 500, 10, objectData + 3814, 0}, + {"audio", "audio", 501, 10, objectData + 3824, 0}, + {"documentPublisher", "documentPublisher", 502, 10, objectData + 3834, 0}, + {"x500UniqueIdentifier", "x500UniqueIdentifier", 503, 3, objectData + 3844, 0}, + {"mime-mhs", "MIME MHS", 504, 5, objectData + 3847, 0}, + {"mime-mhs-headings", "mime-mhs-headings", 505, 6, objectData + 3852, 0}, + {"mime-mhs-bodies", "mime-mhs-bodies", 506, 6, objectData + 3858, 0}, + {"id-hex-partial-message", "id-hex-partial-message", 507, 7, objectData + 3864, 0}, + {"id-hex-multipart-message", "id-hex-multipart-message", 508, 7, objectData + 3871, 0}, + {"generationQualifier", "generationQualifier", 509, 3, objectData + 3878, 0}, + {"pseudonym", "pseudonym", 510, 3, objectData + 3881, 0}, + {nullptr, nullptr, 0, 0, nullptr, 0}, + {"id-set", "Secure Electronic Transactions", 512, 2, objectData + 3884, 0}, + {"set-ctype", "content types", 513, 3, objectData + 3886, 0}, + {"set-msgExt", "message extensions", 514, 3, objectData + 3889, 0}, + {"set-attr", "set-attr", 515, 3, objectData + 3892, 0}, + {"set-policy", "set-policy", 516, 3, objectData + 3895, 0}, + {"set-certExt", "certificate extensions", 517, 3, objectData + 3898, 0}, + {"set-brand", "set-brand", 518, 3, objectData + 3901, 0}, + {"setct-PANData", "setct-PANData", 519, 4, objectData + 3904, 0}, + {"setct-PANToken", "setct-PANToken", 520, 4, objectData + 3908, 0}, + {"setct-PANOnly", "setct-PANOnly", 521, 4, objectData + 3912, 0}, + {"setct-OIData", "setct-OIData", 522, 4, objectData + 3916, 0}, + {"setct-PI", "setct-PI", 523, 4, objectData + 3920, 0}, + {"setct-PIData", "setct-PIData", 524, 4, objectData + 3924, 0}, + {"setct-PIDataUnsigned", "setct-PIDataUnsigned", 525, 4, objectData + 3928, 0}, + {"setct-HODInput", "setct-HODInput", 526, 4, objectData + 3932, 0}, + {"setct-AuthResBaggage", "setct-AuthResBaggage", 527, 4, objectData + 3936, 0}, + {"setct-AuthRevReqBaggage", "setct-AuthRevReqBaggage", 528, 4, objectData + 3940, 0}, + {"setct-AuthRevResBaggage", "setct-AuthRevResBaggage", 529, 4, objectData + 3944, 0}, + {"setct-CapTokenSeq", "setct-CapTokenSeq", 530, 4, objectData + 3948, 0}, + {"setct-PInitResData", "setct-PInitResData", 531, 4, objectData + 3952, 0}, + {"setct-PI-TBS", "setct-PI-TBS", 532, 4, objectData + 3956, 0}, + {"setct-PResData", "setct-PResData", 533, 4, objectData + 3960, 0}, + {"setct-AuthReqTBS", "setct-AuthReqTBS", 534, 4, objectData + 3964, 0}, + {"setct-AuthResTBS", "setct-AuthResTBS", 535, 4, objectData + 3968, 0}, + {"setct-AuthResTBSX", "setct-AuthResTBSX", 536, 4, objectData + 3972, 0}, + {"setct-AuthTokenTBS", "setct-AuthTokenTBS", 537, 4, objectData + 3976, 0}, + {"setct-CapTokenData", "setct-CapTokenData", 538, 4, objectData + 3980, 0}, + {"setct-CapTokenTBS", "setct-CapTokenTBS", 539, 4, objectData + 3984, 0}, + {"setct-AcqCardCodeMsg", "setct-AcqCardCodeMsg", 540, 4, objectData + 3988, 0}, + {"setct-AuthRevReqTBS", "setct-AuthRevReqTBS", 541, 4, objectData + 3992, 0}, + {"setct-AuthRevResData", "setct-AuthRevResData", 542, 4, objectData + 3996, 0}, + {"setct-AuthRevResTBS", "setct-AuthRevResTBS", 543, 4, objectData + 4000, 0}, + {"setct-CapReqTBS", "setct-CapReqTBS", 544, 4, objectData + 4004, 0}, + {"setct-CapReqTBSX", "setct-CapReqTBSX", 545, 4, objectData + 4008, 0}, + {"setct-CapResData", "setct-CapResData", 546, 4, objectData + 4012, 0}, + {"setct-CapRevReqTBS", "setct-CapRevReqTBS", 547, 4, objectData + 4016, 0}, + {"setct-CapRevReqTBSX", "setct-CapRevReqTBSX", 548, 4, objectData + 4020, 0}, + {"setct-CapRevResData", "setct-CapRevResData", 549, 4, objectData + 4024, 0}, + {"setct-CredReqTBS", "setct-CredReqTBS", 550, 4, objectData + 4028, 0}, + {"setct-CredReqTBSX", "setct-CredReqTBSX", 551, 4, objectData + 4032, 0}, + {"setct-CredResData", "setct-CredResData", 552, 4, objectData + 4036, 0}, + {"setct-CredRevReqTBS", "setct-CredRevReqTBS", 553, 4, objectData + 4040, 0}, + {"setct-CredRevReqTBSX", "setct-CredRevReqTBSX", 554, 4, objectData + 4044, 0}, + {"setct-CredRevResData", "setct-CredRevResData", 555, 4, objectData + 4048, 0}, + {"setct-PCertReqData", "setct-PCertReqData", 556, 4, objectData + 4052, 0}, + {"setct-PCertResTBS", "setct-PCertResTBS", 557, 4, objectData + 4056, 0}, + {"setct-BatchAdminReqData", "setct-BatchAdminReqData", 558, 4, objectData + 4060, 0}, + {"setct-BatchAdminResData", "setct-BatchAdminResData", 559, 4, objectData + 4064, 0}, + {"setct-CardCInitResTBS", "setct-CardCInitResTBS", 560, 4, objectData + 4068, 0}, + {"setct-MeAqCInitResTBS", "setct-MeAqCInitResTBS", 561, 4, objectData + 4072, 0}, + {"setct-RegFormResTBS", "setct-RegFormResTBS", 562, 4, objectData + 4076, 0}, + {"setct-CertReqData", "setct-CertReqData", 563, 4, objectData + 4080, 0}, + {"setct-CertReqTBS", "setct-CertReqTBS", 564, 4, objectData + 4084, 0}, + {"setct-CertResData", "setct-CertResData", 565, 4, objectData + 4088, 0}, + {"setct-CertInqReqTBS", "setct-CertInqReqTBS", 566, 4, objectData + 4092, 0}, + {"setct-ErrorTBS", "setct-ErrorTBS", 567, 4, objectData + 4096, 0}, + {"setct-PIDualSignedTBE", "setct-PIDualSignedTBE", 568, 4, objectData + 4100, 0}, + {"setct-PIUnsignedTBE", "setct-PIUnsignedTBE", 569, 4, objectData + 4104, 0}, + {"setct-AuthReqTBE", "setct-AuthReqTBE", 570, 4, objectData + 4108, 0}, + {"setct-AuthResTBE", "setct-AuthResTBE", 571, 4, objectData + 4112, 0}, + {"setct-AuthResTBEX", "setct-AuthResTBEX", 572, 4, objectData + 4116, 0}, + {"setct-AuthTokenTBE", "setct-AuthTokenTBE", 573, 4, objectData + 4120, 0}, + {"setct-CapTokenTBE", "setct-CapTokenTBE", 574, 4, objectData + 4124, 0}, + {"setct-CapTokenTBEX", "setct-CapTokenTBEX", 575, 4, objectData + 4128, 0}, + {"setct-AcqCardCodeMsgTBE", "setct-AcqCardCodeMsgTBE", 576, 4, objectData + 4132, 0}, + {"setct-AuthRevReqTBE", "setct-AuthRevReqTBE", 577, 4, objectData + 4136, 0}, + {"setct-AuthRevResTBE", "setct-AuthRevResTBE", 578, 4, objectData + 4140, 0}, + {"setct-AuthRevResTBEB", "setct-AuthRevResTBEB", 579, 4, objectData + 4144, 0}, + {"setct-CapReqTBE", "setct-CapReqTBE", 580, 4, objectData + 4148, 0}, + {"setct-CapReqTBEX", "setct-CapReqTBEX", 581, 4, objectData + 4152, 0}, + {"setct-CapResTBE", "setct-CapResTBE", 582, 4, objectData + 4156, 0}, + {"setct-CapRevReqTBE", "setct-CapRevReqTBE", 583, 4, objectData + 4160, 0}, + {"setct-CapRevReqTBEX", "setct-CapRevReqTBEX", 584, 4, objectData + 4164, 0}, + {"setct-CapRevResTBE", "setct-CapRevResTBE", 585, 4, objectData + 4168, 0}, + {"setct-CredReqTBE", "setct-CredReqTBE", 586, 4, objectData + 4172, 0}, + {"setct-CredReqTBEX", "setct-CredReqTBEX", 587, 4, objectData + 4176, 0}, + {"setct-CredResTBE", "setct-CredResTBE", 588, 4, objectData + 4180, 0}, + {"setct-CredRevReqTBE", "setct-CredRevReqTBE", 589, 4, objectData + 4184, 0}, + {"setct-CredRevReqTBEX", "setct-CredRevReqTBEX", 590, 4, objectData + 4188, 0}, + {"setct-CredRevResTBE", "setct-CredRevResTBE", 591, 4, objectData + 4192, 0}, + {"setct-BatchAdminReqTBE", "setct-BatchAdminReqTBE", 592, 4, objectData + 4196, 0}, + {"setct-BatchAdminResTBE", "setct-BatchAdminResTBE", 593, 4, objectData + 4200, 0}, + {"setct-RegFormReqTBE", "setct-RegFormReqTBE", 594, 4, objectData + 4204, 0}, + {"setct-CertReqTBE", "setct-CertReqTBE", 595, 4, objectData + 4208, 0}, + {"setct-CertReqTBEX", "setct-CertReqTBEX", 596, 4, objectData + 4212, 0}, + {"setct-CertResTBE", "setct-CertResTBE", 597, 4, objectData + 4216, 0}, + {"setct-CRLNotificationTBS", "setct-CRLNotificationTBS", 598, 4, objectData + 4220, 0}, + {"setct-CRLNotificationResTBS", "setct-CRLNotificationResTBS", 599, 4, objectData + 4224, 0}, + {"setct-BCIDistributionTBS", "setct-BCIDistributionTBS", 600, 4, objectData + 4228, 0}, + {"setext-genCrypt", "generic cryptogram", 601, 4, objectData + 4232, 0}, + {"setext-miAuth", "merchant initiated auth", 602, 4, objectData + 4236, 0}, + {"setext-pinSecure", "setext-pinSecure", 603, 4, objectData + 4240, 0}, + {"setext-pinAny", "setext-pinAny", 604, 4, objectData + 4244, 0}, + {"setext-track2", "setext-track2", 605, 4, objectData + 4248, 0}, + {"setext-cv", "additional verification", 606, 4, objectData + 4252, 0}, + {"set-policy-root", "set-policy-root", 607, 4, objectData + 4256, 0}, + {"setCext-hashedRoot", "setCext-hashedRoot", 608, 4, objectData + 4260, 0}, + {"setCext-certType", "setCext-certType", 609, 4, objectData + 4264, 0}, + {"setCext-merchData", "setCext-merchData", 610, 4, objectData + 4268, 0}, + {"setCext-cCertRequired", "setCext-cCertRequired", 611, 4, objectData + 4272, 0}, + {"setCext-tunneling", "setCext-tunneling", 612, 4, objectData + 4276, 0}, + {"setCext-setExt", "setCext-setExt", 613, 4, objectData + 4280, 0}, + {"setCext-setQualf", "setCext-setQualf", 614, 4, objectData + 4284, 0}, + {"setCext-PGWYcapabilities", "setCext-PGWYcapabilities", 615, 4, objectData + 4288, 0}, + {"setCext-TokenIdentifier", "setCext-TokenIdentifier", 616, 4, objectData + 4292, 0}, + {"setCext-Track2Data", "setCext-Track2Data", 617, 4, objectData + 4296, 0}, + {"setCext-TokenType", "setCext-TokenType", 618, 4, objectData + 4300, 0}, + {"setCext-IssuerCapabilities", "setCext-IssuerCapabilities", 619, 4, objectData + 4304, 0}, + {"setAttr-Cert", "setAttr-Cert", 620, 4, objectData + 4308, 0}, + {"setAttr-PGWYcap", "payment gateway capabilities", 621, 4, objectData + 4312, 0}, + {"setAttr-TokenType", "setAttr-TokenType", 622, 4, objectData + 4316, 0}, + {"setAttr-IssCap", "issuer capabilities", 623, 4, objectData + 4320, 0}, + {"set-rootKeyThumb", "set-rootKeyThumb", 624, 5, objectData + 4324, 0}, + {"set-addPolicy", "set-addPolicy", 625, 5, objectData + 4329, 0}, + {"setAttr-Token-EMV", "setAttr-Token-EMV", 626, 5, objectData + 4334, 0}, + {"setAttr-Token-B0Prime", "setAttr-Token-B0Prime", 627, 5, objectData + 4339, 0}, + {"setAttr-IssCap-CVM", "setAttr-IssCap-CVM", 628, 5, objectData + 4344, 0}, + {"setAttr-IssCap-T2", "setAttr-IssCap-T2", 629, 5, objectData + 4349, 0}, + {"setAttr-IssCap-Sig", "setAttr-IssCap-Sig", 630, 5, objectData + 4354, 0}, + {"setAttr-GenCryptgrm", "generate cryptogram", 631, 6, objectData + 4359, 0}, + {"setAttr-T2Enc", "encrypted track 2", 632, 6, objectData + 4365, 0}, + {"setAttr-T2cleartxt", "cleartext track 2", 633, 6, objectData + 4371, 0}, + {"setAttr-TokICCsig", "ICC or token signature", 634, 6, objectData + 4377, 0}, + {"setAttr-SecDevSig", "secure device signature", 635, 6, objectData + 4383, 0}, + {"set-brand-IATA-ATA", "set-brand-IATA-ATA", 636, 4, objectData + 4389, 0}, + {"set-brand-Diners", "set-brand-Diners", 637, 4, objectData + 4393, 0}, + {"set-brand-AmericanExpress", "set-brand-AmericanExpress", 638, 4, objectData + 4397, 0}, + {"set-brand-JCB", "set-brand-JCB", 639, 4, objectData + 4401, 0}, + {"set-brand-Visa", "set-brand-Visa", 640, 4, objectData + 4405, 0}, + {"set-brand-MasterCard", "set-brand-MasterCard", 641, 4, objectData + 4409, 0}, + {"set-brand-Novus", "set-brand-Novus", 642, 5, objectData + 4413, 0}, + {"DES-CDMF", "des-cdmf", 643, 8, objectData + 4418, 0}, + {"rsaOAEPEncryptionSET", "rsaOAEPEncryptionSET", 644, 9, objectData + 4426, 0}, + {"ITU-T", "itu-t", 645, 0, nullptr, 0}, + {"JOINT-ISO-ITU-T", "joint-iso-itu-t", 646, 0, nullptr, 0}, + {"international-organizations", "International Organizations", 647, 1, objectData + 4435, 0}, + {"msSmartcardLogin", "Microsoft Smartcardlogin", 648, 10, objectData + 4436, 0}, + {"msUPN", "Microsoft Universal Principal Name", 649, 10, objectData + 4446, 0}, + {"AES-128-CFB1", "aes-128-cfb1", 650, 0, nullptr, 0}, + {"AES-192-CFB1", "aes-192-cfb1", 651, 0, nullptr, 0}, + {"AES-256-CFB1", "aes-256-cfb1", 652, 0, nullptr, 0}, + {"AES-128-CFB8", "aes-128-cfb8", 653, 0, nullptr, 0}, + {"AES-192-CFB8", "aes-192-cfb8", 654, 0, nullptr, 0}, + {"AES-256-CFB8", "aes-256-cfb8", 655, 0, nullptr, 0}, + {"DES-CFB1", "des-cfb1", 656, 0, nullptr, 0}, + {"DES-CFB8", "des-cfb8", 657, 0, nullptr, 0}, + {"DES-EDE3-CFB1", "des-ede3-cfb1", 658, 0, nullptr, 0}, + {"DES-EDE3-CFB8", "des-ede3-cfb8", 659, 0, nullptr, 0}, + {"street", "streetAddress", 660, 3, objectData + 4456, 0}, + {"postalCode", "postalCode", 661, 3, objectData + 4459, 0}, + {"id-ppl", "id-ppl", 662, 7, objectData + 4462, 0}, + {"proxyCertInfo", "Proxy Certificate Information", 663, 8, objectData + 4469, 0}, + {"id-ppl-anyLanguage", "Any language", 664, 8, objectData + 4477, 0}, + {"id-ppl-inheritAll", "Inherit all", 665, 8, objectData + 4485, 0}, + {"nameConstraints", "X509v3 Name Constraints", 666, 3, objectData + 4493, 0}, + {"id-ppl-independent", "Independent", 667, 8, objectData + 4496, 0}, + {"RSA-SHA256", "sha256WithRSAEncryption", 668, 9, objectData + 4504, 0}, + {"RSA-SHA384", "sha384WithRSAEncryption", 669, 9, objectData + 4513, 0}, + {"RSA-SHA512", "sha512WithRSAEncryption", 670, 9, objectData + 4522, 0}, + {"RSA-SHA224", "sha224WithRSAEncryption", 671, 9, objectData + 4531, 0}, + {"SHA256", "sha256", 672, 9, objectData + 4540, 0}, + {"SHA384", "sha384", 673, 9, objectData + 4549, 0}, + {"SHA512", "sha512", 674, 9, objectData + 4558, 0}, + {"SHA224", "sha224", 675, 9, objectData + 4567, 0}, + {"identified-organization", "identified-organization", 676, 1, objectData + 4576, 0}, + {"certicom-arc", "certicom-arc", 677, 3, objectData + 4577, 0}, + {"wap", "wap", 678, 2, objectData + 4580, 0}, + {"wap-wsg", "wap-wsg", 679, 3, objectData + 4582, 0}, + {"id-characteristic-two-basis", "id-characteristic-two-basis", 680, 8, objectData + 4585, 0}, + {"onBasis", "onBasis", 681, 9, objectData + 4593, 0}, + {"tpBasis", "tpBasis", 682, 9, objectData + 4602, 0}, + {"ppBasis", "ppBasis", 683, 9, objectData + 4611, 0}, + {"c2pnb163v1", "c2pnb163v1", 684, 8, objectData + 4620, 0}, + {"c2pnb163v2", "c2pnb163v2", 685, 8, objectData + 4628, 0}, + {"c2pnb163v3", "c2pnb163v3", 686, 8, objectData + 4636, 0}, + {"c2pnb176v1", "c2pnb176v1", 687, 8, objectData + 4644, 0}, + {"c2tnb191v1", "c2tnb191v1", 688, 8, objectData + 4652, 0}, + {"c2tnb191v2", "c2tnb191v2", 689, 8, objectData + 4660, 0}, + {"c2tnb191v3", "c2tnb191v3", 690, 8, objectData + 4668, 0}, + {"c2onb191v4", "c2onb191v4", 691, 8, objectData + 4676, 0}, + {"c2onb191v5", "c2onb191v5", 692, 8, objectData + 4684, 0}, + {"c2pnb208w1", "c2pnb208w1", 693, 8, objectData + 4692, 0}, + {"c2tnb239v1", "c2tnb239v1", 694, 8, objectData + 4700, 0}, + {"c2tnb239v2", "c2tnb239v2", 695, 8, objectData + 4708, 0}, + {"c2tnb239v3", "c2tnb239v3", 696, 8, objectData + 4716, 0}, + {"c2onb239v4", "c2onb239v4", 697, 8, objectData + 4724, 0}, + {"c2onb239v5", "c2onb239v5", 698, 8, objectData + 4732, 0}, + {"c2pnb272w1", "c2pnb272w1", 699, 8, objectData + 4740, 0}, + {"c2pnb304w1", "c2pnb304w1", 700, 8, objectData + 4748, 0}, + {"c2tnb359v1", "c2tnb359v1", 701, 8, objectData + 4756, 0}, + {"c2pnb368w1", "c2pnb368w1", 702, 8, objectData + 4764, 0}, + {"c2tnb431r1", "c2tnb431r1", 703, 8, objectData + 4772, 0}, + {"secp112r1", "secp112r1", 704, 5, objectData + 4780, 0}, + {"secp112r2", "secp112r2", 705, 5, objectData + 4785, 0}, + {"secp128r1", "secp128r1", 706, 5, objectData + 4790, 0}, + {"secp128r2", "secp128r2", 707, 5, objectData + 4795, 0}, + {"secp160k1", "secp160k1", 708, 5, objectData + 4800, 0}, + {"secp160r1", "secp160r1", 709, 5, objectData + 4805, 0}, + {"secp160r2", "secp160r2", 710, 5, objectData + 4810, 0}, + {"secp192k1", "secp192k1", 711, 5, objectData + 4815, 0}, + {"secp224k1", "secp224k1", 712, 5, objectData + 4820, 0}, + {"secp224r1", "secp224r1", 713, 5, objectData + 4825, 0}, + {"secp256k1", "secp256k1", 714, 5, objectData + 4830, 0}, + {"secp384r1", "secp384r1", 715, 5, objectData + 4835, 0}, + {"secp521r1", "secp521r1", 716, 5, objectData + 4840, 0}, + {"sect113r1", "sect113r1", 717, 5, objectData + 4845, 0}, + {"sect113r2", "sect113r2", 718, 5, objectData + 4850, 0}, + {"sect131r1", "sect131r1", 719, 5, objectData + 4855, 0}, + {"sect131r2", "sect131r2", 720, 5, objectData + 4860, 0}, + {"sect163k1", "sect163k1", 721, 5, objectData + 4865, 0}, + {"sect163r1", "sect163r1", 722, 5, objectData + 4870, 0}, + {"sect163r2", "sect163r2", 723, 5, objectData + 4875, 0}, + {"sect193r1", "sect193r1", 724, 5, objectData + 4880, 0}, + {"sect193r2", "sect193r2", 725, 5, objectData + 4885, 0}, + {"sect233k1", "sect233k1", 726, 5, objectData + 4890, 0}, + {"sect233r1", "sect233r1", 727, 5, objectData + 4895, 0}, + {"sect239k1", "sect239k1", 728, 5, objectData + 4900, 0}, + {"sect283k1", "sect283k1", 729, 5, objectData + 4905, 0}, + {"sect283r1", "sect283r1", 730, 5, objectData + 4910, 0}, + {"sect409k1", "sect409k1", 731, 5, objectData + 4915, 0}, + {"sect409r1", "sect409r1", 732, 5, objectData + 4920, 0}, + {"sect571k1", "sect571k1", 733, 5, objectData + 4925, 0}, + {"sect571r1", "sect571r1", 734, 5, objectData + 4930, 0}, + {"wap-wsg-idm-ecid-wtls1", "wap-wsg-idm-ecid-wtls1", 735, 5, objectData + 4935, 0}, + {"wap-wsg-idm-ecid-wtls3", "wap-wsg-idm-ecid-wtls3", 736, 5, objectData + 4940, 0}, + {"wap-wsg-idm-ecid-wtls4", "wap-wsg-idm-ecid-wtls4", 737, 5, objectData + 4945, 0}, + {"wap-wsg-idm-ecid-wtls5", "wap-wsg-idm-ecid-wtls5", 738, 5, objectData + 4950, 0}, + {"wap-wsg-idm-ecid-wtls6", "wap-wsg-idm-ecid-wtls6", 739, 5, objectData + 4955, 0}, + {"wap-wsg-idm-ecid-wtls7", "wap-wsg-idm-ecid-wtls7", 740, 5, objectData + 4960, 0}, + {"wap-wsg-idm-ecid-wtls8", "wap-wsg-idm-ecid-wtls8", 741, 5, objectData + 4965, 0}, + {"wap-wsg-idm-ecid-wtls9", "wap-wsg-idm-ecid-wtls9", 742, 5, objectData + 4970, 0}, + {"wap-wsg-idm-ecid-wtls10", "wap-wsg-idm-ecid-wtls10", 743, 5, objectData + 4975, 0}, + {"wap-wsg-idm-ecid-wtls11", "wap-wsg-idm-ecid-wtls11", 744, 5, objectData + 4980, 0}, + {"wap-wsg-idm-ecid-wtls12", "wap-wsg-idm-ecid-wtls12", 745, 5, objectData + 4985, 0}, + {"anyPolicy", "X509v3 Any Policy", 746, 4, objectData + 4990, 0}, + {"policyMappings", "X509v3 Policy Mappings", 747, 3, objectData + 4994, 0}, + {"inhibitAnyPolicy", "X509v3 Inhibit Any Policy", 748, 3, objectData + 4997, 0}, + {"Oakley-EC2N-3", "ipsec3", 749, 0, nullptr, 0}, + {"Oakley-EC2N-4", "ipsec4", 750, 0, nullptr, 0}, + {"CAMELLIA-128-CBC", "camellia-128-cbc", 751, 11, objectData + 5000, 0}, + {"CAMELLIA-192-CBC", "camellia-192-cbc", 752, 11, objectData + 5011, 0}, + {"CAMELLIA-256-CBC", "camellia-256-cbc", 753, 11, objectData + 5022, 0}, + {"CAMELLIA-128-ECB", "camellia-128-ecb", 754, 8, objectData + 5033, 0}, + {"CAMELLIA-192-ECB", "camellia-192-ecb", 755, 8, objectData + 5041, 0}, + {"CAMELLIA-256-ECB", "camellia-256-ecb", 756, 8, objectData + 5049, 0}, + {"CAMELLIA-128-CFB", "camellia-128-cfb", 757, 8, objectData + 5057, 0}, + {"CAMELLIA-192-CFB", "camellia-192-cfb", 758, 8, objectData + 5065, 0}, + {"CAMELLIA-256-CFB", "camellia-256-cfb", 759, 8, objectData + 5073, 0}, + {"CAMELLIA-128-CFB1", "camellia-128-cfb1", 760, 0, nullptr, 0}, + {"CAMELLIA-192-CFB1", "camellia-192-cfb1", 761, 0, nullptr, 0}, + {"CAMELLIA-256-CFB1", "camellia-256-cfb1", 762, 0, nullptr, 0}, + {"CAMELLIA-128-CFB8", "camellia-128-cfb8", 763, 0, nullptr, 0}, + {"CAMELLIA-192-CFB8", "camellia-192-cfb8", 764, 0, nullptr, 0}, + {"CAMELLIA-256-CFB8", "camellia-256-cfb8", 765, 0, nullptr, 0}, + {"CAMELLIA-128-OFB", "camellia-128-ofb", 766, 8, objectData + 5081, 0}, + {"CAMELLIA-192-OFB", "camellia-192-ofb", 767, 8, objectData + 5089, 0}, + {"CAMELLIA-256-OFB", "camellia-256-ofb", 768, 8, objectData + 5097, 0}, + {"subjectDirectoryAttributes", "X509v3 Subject Directory Attributes", 769, 3, objectData + 5105, 0}, + {"issuingDistributionPoint", "X509v3 Issuing Distribution Point", 770, 3, objectData + 5108, 0}, + {"certificateIssuer", "X509v3 Certificate Issuer", 771, 3, objectData + 5111, 0}, + {nullptr, nullptr, 0, 0, nullptr, 0}, + {"KISA", "kisa", 773, 6, objectData + 5114, 0}, + {nullptr, nullptr, 0, 0, nullptr, 0}, + {nullptr, nullptr, 0, 0, nullptr, 0}, + {"SEED-ECB", "seed-ecb", 776, 8, objectData + 5120, 0}, + {"SEED-CBC", "seed-cbc", 777, 8, objectData + 5128, 0}, + {"SEED-OFB", "seed-ofb", 778, 8, objectData + 5136, 0}, + {"SEED-CFB", "seed-cfb", 779, 8, objectData + 5144, 0}, + {"HMAC-MD5", "hmac-md5", 780, 8, objectData + 5152, 0}, + {"HMAC-SHA1", "hmac-sha1", 781, 8, objectData + 5160, 0}, + {"id-PasswordBasedMAC", "password based MAC", 782, 9, objectData + 5168, 0}, + {"id-DHBasedMac", "Diffie-Hellman based MAC", 783, 9, objectData + 5177, 0}, + {"id-it-suppLangTags", "id-it-suppLangTags", 784, 8, objectData + 5186, 0}, + {"caRepository", "CA Repository", 785, 8, objectData + 5194, 0}, + {"id-smime-ct-compressedData", "id-smime-ct-compressedData", 786, 11, objectData + 5202, 0}, + {"id-ct-asciiTextWithCRLF", "id-ct-asciiTextWithCRLF", 787, 11, objectData + 5213, 0}, + {"id-aes128-wrap", "id-aes128-wrap", 788, 9, objectData + 5224, 0}, + {"id-aes192-wrap", "id-aes192-wrap", 789, 9, objectData + 5233, 0}, + {"id-aes256-wrap", "id-aes256-wrap", 790, 9, objectData + 5242, 0}, + {"ecdsa-with-Recommended", "ecdsa-with-Recommended", 791, 7, objectData + 5251, 0}, + {"ecdsa-with-Specified", "ecdsa-with-Specified", 792, 7, objectData + 5258, 0}, + {"ecdsa-with-SHA224", "ecdsa-with-SHA224", 793, 8, objectData + 5265, 0}, + {"ecdsa-with-SHA256", "ecdsa-with-SHA256", 794, 8, objectData + 5273, 0}, + {"ecdsa-with-SHA384", "ecdsa-with-SHA384", 795, 8, objectData + 5281, 0}, + {"ecdsa-with-SHA512", "ecdsa-with-SHA512", 796, 8, objectData + 5289, 0}, + {"hmacWithMD5", "hmacWithMD5", 797, 8, objectData + 5297, 0}, + {"hmacWithSHA224", "hmacWithSHA224", 798, 8, objectData + 5305, 0}, + {"hmacWithSHA256", "hmacWithSHA256", 799, 8, objectData + 5313, 0}, + {"hmacWithSHA384", "hmacWithSHA384", 800, 8, objectData + 5321, 0}, + {"hmacWithSHA512", "hmacWithSHA512", 801, 8, objectData + 5329, 0}, + {"dsa_with_SHA224", "dsa_with_SHA224", 802, 9, objectData + 5337, 0}, + {"dsa_with_SHA256", "dsa_with_SHA256", 803, 9, objectData + 5346, 0}, + {"whirlpool", "whirlpool", 804, 6, objectData + 5355, 0}, + {"cryptopro", "cryptopro", 805, 5, objectData + 5361, 0}, + {"cryptocom", "cryptocom", 806, 5, objectData + 5366, 0}, + {"id-GostR3411-94-with-GostR3410-2001", "GOST R 34.11-94 with GOST R 34.10-2001", 807, 6, objectData + 5371, 0}, + {"id-GostR3411-94-with-GostR3410-94", "GOST R 34.11-94 with GOST R 34.10-94", 808, 6, objectData + 5377, 0}, + {"md_gost94", "GOST R 34.11-94", 809, 6, objectData + 5383, 0}, + {"id-HMACGostR3411-94", "HMAC GOST 34.11-94", 810, 6, objectData + 5389, 0}, + {"gost2001", "GOST R 34.10-2001", 811, 6, objectData + 5395, 0}, + {"gost94", "GOST R 34.10-94", 812, 6, objectData + 5401, 0}, + {"gost89", "GOST 28147-89", 813, 6, objectData + 5407, 0}, + {"gost89-cnt", "gost89-cnt", 814, 0, nullptr, 0}, + {"gost-mac", "GOST 28147-89 MAC", 815, 6, objectData + 5413, 0}, + {"prf-gostr3411-94", "GOST R 34.11-94 PRF", 816, 6, objectData + 5419, 0}, + {"id-GostR3410-2001DH", "GOST R 34.10-2001 DH", 817, 6, objectData + 5425, 0}, + {"id-GostR3410-94DH", "GOST R 34.10-94 DH", 818, 6, objectData + 5431, 0}, + {"id-Gost28147-89-CryptoPro-KeyMeshing", "id-Gost28147-89-CryptoPro-KeyMeshing", 819, 7, objectData + 5437, 0}, + {"id-Gost28147-89-None-KeyMeshing", "id-Gost28147-89-None-KeyMeshing", 820, 7, objectData + 5444, 0}, + {"id-GostR3411-94-TestParamSet", "id-GostR3411-94-TestParamSet", 821, 7, objectData + 5451, 0}, + {"id-GostR3411-94-CryptoProParamSet", "id-GostR3411-94-CryptoProParamSet", 822, 7, objectData + 5458, 0}, + {"id-Gost28147-89-TestParamSet", "id-Gost28147-89-TestParamSet", 823, 7, objectData + 5465, 0}, + {"id-Gost28147-89-CryptoPro-A-ParamSet", "id-Gost28147-89-CryptoPro-A-ParamSet", 824, 7, objectData + 5472, 0}, + {"id-Gost28147-89-CryptoPro-B-ParamSet", "id-Gost28147-89-CryptoPro-B-ParamSet", 825, 7, objectData + 5479, 0}, + {"id-Gost28147-89-CryptoPro-C-ParamSet", "id-Gost28147-89-CryptoPro-C-ParamSet", 826, 7, objectData + 5486, 0}, + {"id-Gost28147-89-CryptoPro-D-ParamSet", "id-Gost28147-89-CryptoPro-D-ParamSet", 827, 7, objectData + 5493, 0}, + {"id-Gost28147-89-CryptoPro-Oscar-1-1-ParamSet", "id-Gost28147-89-CryptoPro-Oscar-1-1-ParamSet", 828, 7, objectData + 5500, 0}, + {"id-Gost28147-89-CryptoPro-Oscar-1-0-ParamSet", "id-Gost28147-89-CryptoPro-Oscar-1-0-ParamSet", 829, 7, objectData + 5507, 0}, + {"id-Gost28147-89-CryptoPro-RIC-1-ParamSet", "id-Gost28147-89-CryptoPro-RIC-1-ParamSet", 830, 7, objectData + 5514, 0}, + {"id-GostR3410-94-TestParamSet", "id-GostR3410-94-TestParamSet", 831, 7, objectData + 5521, 0}, + {"id-GostR3410-94-CryptoPro-A-ParamSet", "id-GostR3410-94-CryptoPro-A-ParamSet", 832, 7, objectData + 5528, 0}, + {"id-GostR3410-94-CryptoPro-B-ParamSet", "id-GostR3410-94-CryptoPro-B-ParamSet", 833, 7, objectData + 5535, 0}, + {"id-GostR3410-94-CryptoPro-C-ParamSet", "id-GostR3410-94-CryptoPro-C-ParamSet", 834, 7, objectData + 5542, 0}, + {"id-GostR3410-94-CryptoPro-D-ParamSet", "id-GostR3410-94-CryptoPro-D-ParamSet", 835, 7, objectData + 5549, 0}, + {"id-GostR3410-94-CryptoPro-XchA-ParamSet", "id-GostR3410-94-CryptoPro-XchA-ParamSet", 836, 7, objectData + 5556, 0}, + {"id-GostR3410-94-CryptoPro-XchB-ParamSet", "id-GostR3410-94-CryptoPro-XchB-ParamSet", 837, 7, objectData + 5563, 0}, + {"id-GostR3410-94-CryptoPro-XchC-ParamSet", "id-GostR3410-94-CryptoPro-XchC-ParamSet", 838, 7, objectData + 5570, 0}, + {"id-GostR3410-2001-TestParamSet", "id-GostR3410-2001-TestParamSet", 839, 7, objectData + 5577, 0}, + {"id-GostR3410-2001-CryptoPro-A-ParamSet", "id-GostR3410-2001-CryptoPro-A-ParamSet", 840, 7, objectData + 5584, 0}, + {"id-GostR3410-2001-CryptoPro-B-ParamSet", "id-GostR3410-2001-CryptoPro-B-ParamSet", 841, 7, objectData + 5591, 0}, + {"id-GostR3410-2001-CryptoPro-C-ParamSet", "id-GostR3410-2001-CryptoPro-C-ParamSet", 842, 7, objectData + 5598, 0}, + {"id-GostR3410-2001-CryptoPro-XchA-ParamSet", "id-GostR3410-2001-CryptoPro-XchA-ParamSet", 843, 7, objectData + 5605, 0}, + {"id-GostR3410-2001-CryptoPro-XchB-ParamSet", "id-GostR3410-2001-CryptoPro-XchB-ParamSet", 844, 7, objectData + 5612, 0}, + {"id-GostR3410-94-a", "id-GostR3410-94-a", 845, 7, objectData + 5619, 0}, + {"id-GostR3410-94-aBis", "id-GostR3410-94-aBis", 846, 7, objectData + 5626, 0}, + {"id-GostR3410-94-b", "id-GostR3410-94-b", 847, 7, objectData + 5633, 0}, + {"id-GostR3410-94-bBis", "id-GostR3410-94-bBis", 848, 7, objectData + 5640, 0}, + {"id-Gost28147-89-cc", "GOST 28147-89 Cryptocom ParamSet", 849, 8, objectData + 5647, 0}, + {"gost94cc", "GOST 34.10-94 Cryptocom", 850, 8, objectData + 5655, 0}, + {"gost2001cc", "GOST 34.10-2001 Cryptocom", 851, 8, objectData + 5663, 0}, + {"id-GostR3411-94-with-GostR3410-94-cc", "GOST R 34.11-94 with GOST R 34.10-94 Cryptocom", 852, 8, objectData + 5671, 0}, + {"id-GostR3411-94-with-GostR3410-2001-cc", "GOST R 34.11-94 with GOST R 34.10-2001 Cryptocom", 853, 8, objectData + 5679, 0}, + {"id-GostR3410-2001-ParamSet-cc", "GOST R 3410-2001 Parameter Set Cryptocom", 854, 8, objectData + 5687, 0}, + {"HMAC", "hmac", 855, 0, nullptr, 0}, + {"LocalKeySet", "Microsoft Local Key set", 856, 9, objectData + 5695, 0}, + {"freshestCRL", "X509v3 Freshest CRL", 857, 3, objectData + 5704, 0}, + {"id-on-permanentIdentifier", "Permanent Identifier", 858, 8, objectData + 5707, 0}, + {"searchGuide", "searchGuide", 859, 3, objectData + 5715, 0}, + {"businessCategory", "businessCategory", 860, 3, objectData + 5718, 0}, + {"postalAddress", "postalAddress", 861, 3, objectData + 5721, 0}, + {"postOfficeBox", "postOfficeBox", 862, 3, objectData + 5724, 0}, + {"physicalDeliveryOfficeName", "physicalDeliveryOfficeName", 863, 3, objectData + 5727, 0}, + {"telephoneNumber", "telephoneNumber", 864, 3, objectData + 5730, 0}, + {"telexNumber", "telexNumber", 865, 3, objectData + 5733, 0}, + {"teletexTerminalIdentifier", "teletexTerminalIdentifier", 866, 3, objectData + 5736, 0}, + {"facsimileTelephoneNumber", "facsimileTelephoneNumber", 867, 3, objectData + 5739, 0}, + {"x121Address", "x121Address", 868, 3, objectData + 5742, 0}, + {"internationaliSDNNumber", "internationaliSDNNumber", 869, 3, objectData + 5745, 0}, + {"registeredAddress", "registeredAddress", 870, 3, objectData + 5748, 0}, + {"destinationIndicator", "destinationIndicator", 871, 3, objectData + 5751, 0}, + {"preferredDeliveryMethod", "preferredDeliveryMethod", 872, 3, objectData + 5754, 0}, + {"presentationAddress", "presentationAddress", 873, 3, objectData + 5757, 0}, + {"supportedApplicationContext", "supportedApplicationContext", 874, 3, objectData + 5760, 0}, + {"member", "member", 875, 3, objectData + 5763, 0}, + {"owner", "owner", 876, 3, objectData + 5766, 0}, + {"roleOccupant", "roleOccupant", 877, 3, objectData + 5769, 0}, + {"seeAlso", "seeAlso", 878, 3, objectData + 5772, 0}, + {"userPassword", "userPassword", 879, 3, objectData + 5775, 0}, + {"userCertificate", "userCertificate", 880, 3, objectData + 5778, 0}, + {"cACertificate", "cACertificate", 881, 3, objectData + 5781, 0}, + {"authorityRevocationList", "authorityRevocationList", 882, 3, objectData + 5784, 0}, + {"certificateRevocationList", "certificateRevocationList", 883, 3, objectData + 5787, 0}, + {"crossCertificatePair", "crossCertificatePair", 884, 3, objectData + 5790, 0}, + {"enhancedSearchGuide", "enhancedSearchGuide", 885, 3, objectData + 5793, 0}, + {"protocolInformation", "protocolInformation", 886, 3, objectData + 5796, 0}, + {"distinguishedName", "distinguishedName", 887, 3, objectData + 5799, 0}, + {"uniqueMember", "uniqueMember", 888, 3, objectData + 5802, 0}, + {"houseIdentifier", "houseIdentifier", 889, 3, objectData + 5805, 0}, + {"supportedAlgorithms", "supportedAlgorithms", 890, 3, objectData + 5808, 0}, + {"deltaRevocationList", "deltaRevocationList", 891, 3, objectData + 5811, 0}, + {"dmdName", "dmdName", 892, 3, objectData + 5814, 0}, + {"id-alg-PWRI-KEK", "id-alg-PWRI-KEK", 893, 11, objectData + 5817, 0}, + {"CMAC", "cmac", 894, 0, nullptr, 0}, + {"id-aes128-GCM", "aes-128-gcm", 895, 9, objectData + 5828, 0}, + {"id-aes128-CCM", "aes-128-ccm", 896, 9, objectData + 5837, 0}, + {"id-aes128-wrap-pad", "id-aes128-wrap-pad", 897, 9, objectData + 5846, 0}, + {"id-aes192-GCM", "aes-192-gcm", 898, 9, objectData + 5855, 0}, + {"id-aes192-CCM", "aes-192-ccm", 899, 9, objectData + 5864, 0}, + {"id-aes192-wrap-pad", "id-aes192-wrap-pad", 900, 9, objectData + 5873, 0}, + {"id-aes256-GCM", "aes-256-gcm", 901, 9, objectData + 5882, 0}, + {"id-aes256-CCM", "aes-256-ccm", 902, 9, objectData + 5891, 0}, + {"id-aes256-wrap-pad", "id-aes256-wrap-pad", 903, 9, objectData + 5900, 0}, + {"AES-128-CTR", "aes-128-ctr", 904, 0, nullptr, 0}, + {"AES-192-CTR", "aes-192-ctr", 905, 0, nullptr, 0}, + {"AES-256-CTR", "aes-256-ctr", 906, 0, nullptr, 0}, + {"id-camellia128-wrap", "id-camellia128-wrap", 907, 11, objectData + 5909, 0}, + {"id-camellia192-wrap", "id-camellia192-wrap", 908, 11, objectData + 5920, 0}, + {"id-camellia256-wrap", "id-camellia256-wrap", 909, 11, objectData + 5931, 0}, + {"anyExtendedKeyUsage", "Any Extended Key Usage", 910, 4, objectData + 5942, 0}, + {"MGF1", "mgf1", 911, 9, objectData + 5946, 0}, + {"RSASSA-PSS", "rsassaPss", 912, 9, objectData + 5955, 0}, + {"AES-128-XTS", "aes-128-xts", 913, 0, nullptr, 0}, + {"AES-256-XTS", "aes-256-xts", 914, 0, nullptr, 0}, + {"RC4-HMAC-MD5", "rc4-hmac-md5", 915, 0, nullptr, 0}, + {"AES-128-CBC-HMAC-SHA1", "aes-128-cbc-hmac-sha1", 916, 0, nullptr, 0}, + {"AES-192-CBC-HMAC-SHA1", "aes-192-cbc-hmac-sha1", 917, 0, nullptr, 0}, + {"AES-256-CBC-HMAC-SHA1", "aes-256-cbc-hmac-sha1", 918, 0, nullptr, 0}, + {"RSAES-OAEP", "rsaesOaep", 919, 9, objectData + 5964, 0}, + {"teletrust", "teletrust", 920, 2, objectData + 5973, 0}, + {"brainpool", "brainpool", 921, 7, objectData + 5975, 0}, + {"brainpoolP160r1", "brainpoolP160r1", 922, 9, objectData + 5982, 0}, + {"brainpoolP160t1", "brainpoolP160t1", 923, 9, objectData + 5991, 0}, + {"brainpoolP192r1", "brainpoolP192r1", 924, 9, objectData + 6000, 0}, + {"brainpoolP192t1", "brainpoolP192t1", 925, 9, objectData + 6009, 0}, + {"brainpoolP224r1", "brainpoolP224r1", 926, 9, objectData + 6018, 0}, + {"brainpoolP224t1", "brainpoolP224t1", 927, 9, objectData + 6027, 0}, + {"brainpoolP256r1", "brainpoolP256r1", 928, 9, objectData + 6036, 0}, + {"brainpoolP256t1", "brainpoolP256t1", 929, 9, objectData + 6045, 0}, + {"brainpoolP320r1", "brainpoolP320r1", 930, 9, objectData + 6054, 0}, + {"brainpoolP320t1", "brainpoolP320t1", 931, 9, objectData + 6063, 0}, + {"brainpoolP384r1", "brainpoolP384r1", 932, 9, objectData + 6072, 0}, + {"brainpoolP384t1", "brainpoolP384t1", 933, 9, objectData + 6081, 0}, + {"brainpoolP512r1", "brainpoolP512r1", 934, 9, objectData + 6090, 0}, + {"brainpoolP512t1", "brainpoolP512t1", 935, 9, objectData + 6099, 0}, + {"FRP256v1", "FRP256v1", 936, 10, objectData + 6108, 0}, + {"ChaCha", "chacha", 937, 0, nullptr, 0}, + {"gost89-ecb", "gost89-ecb", 938, 0, nullptr, 0}, + {"gost89-cbc", "gost89-cbc", 939, 0, nullptr, 0}, + {"tc26", "tc26", 940, 5, objectData + 6118, 0}, + {"streebog256", "GOST R 34.11-2012 (256 bit)", 941, 8, objectData + 6123, 0}, + {"streebog512", "GOST R 34-11-2012 (512 bit)", 942, 8, objectData + 6131, 0}, + {"id-tc26-gost-3410-2012-512-paramSetA", "id-tc26-gost-3410-2012-512-paramSetA", 943, 9, objectData + 6139, 0}, + {"id-tc26-gost-3410-2012-512-paramSetB", "id-tc26-gost-3410-2012-512-paramSetB", 944, 9, objectData + 6148, 0}, + {"id-tc26-gost-28147-param-Z", "id-tc26-gost-28147-param-Z", 945, 9, objectData + 6157, 0}, + {"id-tc26-gost3410-2012-256", "GOST R 34.10-2012 (256 bit)", 946, 8, objectData + 6166, 0}, + {"id-tc26-gost3410-2012-512", "GOST R 34.10-2012 (512 bit)", 947, 8, objectData + 6174, 0}, + {"id-tc26-signwithdigest-gost3410-2012-256", "GOST R 34.11-2012 with GOST R 34.10-2012 (256 bit)", 948, 8, objectData + 6182, 0}, + {"id-tc26-signwithdigest-gost3410-2012-512", "GOST R 34.11-2012 with GOST R 34.10-2012 (512 bit)", 949, 8, objectData + 6190, 0}, + {"X25519", "X25519", 950, 3, objectData + 6198, 0}, + {"X448", "X448", 951, 3, objectData + 6201, 0}, + {"Ed25519", "Ed25519", 952, 3, objectData + 6204, 0}, + {"Ed448", "Ed448", 953, 3, objectData + 6207, 0}, + {"Ed25519ph", "Ed25519ph", 954, 3, objectData + 6210, 0}, + {"Ed448ph", "Ed448ph", 955, 3, objectData + 6213, 0}, +}; + +const unsigned short LibreSslObjectOrder_nid_no_patch[LibreSslObjectOrderCount] = { + 0, 181, 393, 404, 645, 646, 434, 182, 379, 676, 11, 647, 380, 920, 12, 378, + 81, 512, 678, 435, 183, 381, 950, 951, 952, 953, 954, 955, 677, 394, 13, 100, + 105, 14, 15, 16, 660, 17, 18, 106, 107, 859, 860, 861, 661, 862, 863, 864, + 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, + 881, 882, 883, 884, 173, 99, 101, 509, 503, 174, 885, 886, 887, 888, 889, 890, + 891, 892, 510, 400, 769, 82, 83, 84, 85, 86, 87, 88, 141, 430, 142, 140, + 770, 771, 666, 103, 89, 747, 90, 401, 126, 857, 748, 402, 403, 513, 514, 515, + 516, 517, 518, 679, 382, 383, 384, 385, 386, 387, 388, 376, 395, 19, 96, 95, + 746, 910, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, + 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, + 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, + 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, + 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, + 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 620, 621, 622, 623, 607, 608, + 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 636, 640, 641, 637, 638, + 639, 805, 806, 940, 184, 405, 389, 504, 104, 29, 31, 45, 30, 377, 67, 66, + 42, 32, 41, 64, 70, 115, 117, 143, 721, 722, 728, 717, 718, 704, 705, 709, + 708, 714, 723, 729, 730, 719, 720, 724, 725, 726, 727, 706, 707, 710, 711, 712, + 713, 715, 716, 731, 732, 733, 734, 624, 625, 626, 627, 628, 629, 630, 642, 735, + 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 804, 124, 773, 807, 808, 809, + 810, 811, 812, 813, 815, 816, 817, 818, 1, 185, 127, 505, 506, 119, 631, 632, + 633, 634, 635, 436, 820, 819, 845, 846, 847, 848, 821, 822, 823, 824, 825, 826, + 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, + 843, 844, 2, 431, 432, 433, 116, 113, 406, 407, 408, 416, 791, 792, 258, 175, + 259, 128, 260, 261, 262, 263, 264, 265, 266, 267, 268, 662, 176, 507, 508, 921, + 57, 754, 766, 757, 755, 767, 758, 756, 768, 759, 437, 776, 777, 779, 778, 852, + 853, 850, 851, 849, 854, 946, 947, 941, 942, 948, 949, 186, 27, 187, 20, 47, + 3, 257, 4, 797, 163, 798, 799, 800, 801, 37, 5, 44, 120, 643, 680, 684, + 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, + 701, 702, 703, 409, 410, 411, 412, 413, 414, 415, 793, 794, 795, 796, 269, 270, + 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 177, 285, + 286, 287, 288, 289, 290, 291, 292, 397, 398, 663, 164, 165, 293, 129, 130, 131, + 132, 294, 295, 296, 133, 180, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, + 307, 308, 309, 310, 311, 312, 784, 313, 314, 323, 324, 325, 326, 327, 328, 329, + 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, + 346, 347, 858, 348, 349, 351, 352, 353, 354, 355, 356, 357, 358, 399, 359, 360, + 361, 362, 664, 665, 667, 178, 179, 363, 364, 785, 780, 781, 58, 59, 438, 439, + 440, 441, 943, 944, 945, 108, 112, 782, 783, 6, 7, 396, 8, 65, 644, 919, + 911, 912, 668, 669, 670, 671, 28, 9, 10, 168, 169, 170, 68, 69, 161, 162, + 21, 22, 23, 24, 25, 26, 48, 49, 50, 51, 52, 53, 54, 55, 56, 172, + 167, 188, 156, 157, 681, 682, 683, 417, 856, 390, 91, 315, 316, 317, 318, 319, + 320, 321, 322, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 922, 923, + 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 418, 419, 420, 421, + 788, 895, 896, 897, 422, 423, 424, 425, 789, 898, 899, 900, 426, 427, 428, 429, + 790, 901, 902, 903, 672, 673, 674, 675, 802, 803, 71, 72, 73, 74, 75, 76, + 77, 78, 79, 139, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, + 470, 471, 472, 473, 474, 475, 476, 477, 391, 478, 479, 480, 481, 482, 483, 484, + 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, + 501, 502, 442, 443, 444, 445, 446, 447, 448, 449, 392, 450, 451, 452, 453, 454, + 455, 456, 457, 936, 189, 190, 191, 192, 193, 194, 195, 158, 159, 160, 144, 145, + 146, 147, 148, 149, 171, 134, 135, 136, 137, 138, 648, 649, 751, 752, 753, 907, + 908, 909, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, + 210, 211, 786, 787, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 125, 893, 248, 249, 250, 251, 252, 253, + 254, 255, 256, 150, 151, 152, 153, 154, 155, 34, +}; + +} diff --git a/core/libs/prx/libSceLibreSsl/src/ObjectText.cpp b/core/libs/prx/libSceLibreSsl/src/ObjectText.cpp new file mode 100644 index 0000000000..e41bbdd11c --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/ObjectText.cpp @@ -0,0 +1,207 @@ +#include "prx/libSceLibreSsl/include/ObjectTable.hpp" +#include "prx/libSceLibreSsl/include/Error.hpp" +#include "prx/libc/include/General.hpp" +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <cstring> +#include <limits> +#include <string> +#include <vector> + +static_assert(sizeof(LibreSslAsn1Object) == 40); +static_assert(offsetof(LibreSslAsn1Object, longName) == 8); +static_assert(offsetof(LibreSslAsn1Object, nid) == 16); +static_assert(offsetof(LibreSslAsn1Object, length) == 20); +static_assert(offsetof(LibreSslAsn1Object, data) == 24); +static_assert(offsetof(LibreSslAsn1Object, flags) == 32); + +namespace { + +constexpr int ObjectLibrary = 8; +constexpr int AnyFunction = 0xfff; +constexpr int UnknownNid = 101; + +class BigNumber { +public: + explicit BigNumber(std::uint64_t value) : limbs{static_cast<std::uint32_t>(value), static_cast<std::uint32_t>(value >> 32)} {} + + void ShiftLeft7() { + std::uint32_t carry = 0; + for (auto& limb : limbs) { + const std::uint32_t next = limb >> 25; + limb = (limb << 7) | carry; + carry = next; + } + if (carry) limbs.push_back(carry); + } + + void Add(std::uint32_t value) { + for (auto& limb : limbs) { + const std::uint64_t sum = static_cast<std::uint64_t>(limb) + value; + limb = static_cast<std::uint32_t>(sum); + value = static_cast<std::uint32_t>(sum >> 32); + if (!value) return; + } + limbs.push_back(value); + } + + void Subtract(std::uint32_t value) { + for (auto& limb : limbs) { + const std::uint32_t before = limb; + limb -= value; + if (before >= value) return; + value = 1; + } + } + + std::string Decimal() const { + std::vector<std::uint32_t> number = limbs; + std::string digits; + while (std::any_of(number.begin(), number.end(), [](std::uint32_t limb) { return limb != 0; })) { + std::uint64_t remainder = 0; + for (auto limb = number.rbegin(); limb != number.rend(); ++limb) { + const std::uint64_t current = (remainder << 32) | *limb; + *limb = static_cast<std::uint32_t>(current / 10); + remainder = current % 10; + } + digits.push_back(static_cast<char>('0' + remainder)); + } + if (digits.empty()) digits = "0"; + std::reverse(digits.begin(), digits.end()); + return digits; + } + +private: + std::vector<std::uint32_t> limbs; +}; + +int CompareObject(const LibreSslAsn1Object* key, const LibreSslAsn1Object* entry) { + const int difference = key->length - entry->length; + if (difference) return difference; + return key->length > 0 ? std::memcmp(key->data, entry->data, static_cast<std::size_t>(key->length)) : 0; +} + +const char* Name(int nid, bool longName) { + if (nid < 0 || nid >= LibreSslObjectCount) return nullptr; + const auto& object = LibreSslObjects_nid_no_patch[nid]; + if (nid != 0 && object.nid == 0) { + LibreSslPutError_nid_no_patch(ObjectLibrary, AnyFunction, UnknownNid); + return nullptr; + } + return longName ? object.longName : object.shortName; +} + +void Write(char*& buffer, int& space, const std::string& text) { + if (space > 0) { + const std::size_t copied = std::min(text.size(), static_cast<std::size_t>(space - 1)); + std::memcpy(buffer, text.data(), copied); + buffer[copied] = '\0'; + } + const int written = static_cast<int>(text.size()); + if (written >= space) { + space = 0; + } else { + buffer += written; + space -= written; + } +} + +int ObjectText(char* buffer, int space, const LibreSslAsn1Object* object, const char* function) { + if (space < 0 || (space > 0 && !buffer)) NotImplemented_nid_no_patch(function); + if (space > 0) buffer[0] = '\0'; + if (!object || !object->data) return 0; + const int nid = LibreSslObjectNid_nid_no_patch(object); + if (nid != 0) { + const char* name = LibreSslObjectLongName_nid_no_patch(nid); + if (!name) name = LibreSslObjectShortName_nid_no_patch(nid); + if (name) { + const std::size_t length = std::strlen(name); + if (space > 0) { + const std::size_t copied = std::min(length, static_cast<std::size_t>(space - 1)); + std::memcpy(buffer, name, copied); + buffer[copied] = '\0'; + } + return static_cast<int>(length); + } + } + int result = 0; + int remaining = object->length; + const unsigned char* bytes = object->data; + bool first = true; + while (remaining > 0) { + std::uint64_t value = 0; + bool big = false; + BigNumber number(0); + for (;;) { + const unsigned char byte = *bytes++; + --remaining; + if (remaining == 0 && (byte & 0x80)) return 0; + if (big) number.Add(byte & 0x7f); + else value |= byte & 0x7f; + if (!(byte & 0x80)) break; + if (!big && value > (std::numeric_limits<std::uint64_t>::max() >> 7)) { + number = BigNumber(value); + big = true; + } + if (big) number.ShiftLeft7(); + else value <<= 7; + } + if (first) { + first = false; + int arc; + if (value >= 80) { + arc = 2; + if (big) number.Subtract(80); + else value -= 80; + } else { + arc = static_cast<int>(value / 40); + value -= static_cast<std::uint64_t>(arc) * 40; + } + if (space > 1) { + *buffer++ = static_cast<char>('0' + arc); + *buffer = '\0'; + --space; + } + ++result; + } + const std::string text = "." + (big ? number.Decimal() : std::to_string(value)); + Write(buffer, space, text); + result += static_cast<int>(text.size()); + } + return result; +} + +} // namespace + +extern "C" { + +int LibreSslObjectNid_nid_no_patch(const LibreSslAsn1Object* object) { + if (!object) return 0; + if (object->nid != 0) return object->nid; + int low = 0; + int high = LibreSslObjectOrderCount; + while (low < high) { + const int middle = (low + high) / 2; + const auto& entry = LibreSslObjects_nid_no_patch[LibreSslObjectOrder_nid_no_patch[middle]]; + const int comparison = CompareObject(object, &entry); + if (comparison < 0) high = middle; + else if (comparison > 0) low = middle + 1; + else return entry.nid; + } + return 0; +} + +const char* LibreSslObjectShortName_nid_no_patch(int nid) { + return Name(nid, false); +} + +const char* LibreSslObjectLongName_nid_no_patch(int nid) { + return Name(nid, true); +} + +int APS5_VABI i2t_ASN1_OBJECT_nid_postfix(char* buffer, int length, const LibreSslAsn1Object* object) { + return ObjectText(buffer, length, object, __func__); +} + +} diff --git a/core/libs/prx/libSceLibreSsl/tests/ObjectText.cpp b/core/libs/prx/libSceLibreSsl/tests/ObjectText.cpp new file mode 100644 index 0000000000..b0f93aa427 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/tests/ObjectText.cpp @@ -0,0 +1,138 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libSceLibreSsl/include/ObjectTable.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include <array> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <exception> +#include <initializer_list> +#include <string> +#include <vector> + +namespace { + +constexpr std::uint64_t UnknownNidError = (8ull << 24) | (0xfffull << 12) | 101; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +template<typename TAction> +void RequireThrows(TAction action, const char* message) { + try { + action(); + } catch (const std::exception&) { + return; + } + Require(false, message); +} + +struct Encoded { + std::vector<unsigned char> bytes; + LibreSslAsn1Object object; + + Encoded(std::initializer_list<unsigned char> data, int nid = 0) : bytes(data), object{nullptr, nullptr, nid, static_cast<int>(bytes.size()), bytes.data(), 0} {} +}; + +void RequireText(const LibreSslAsn1Object& object, int space, int expected, const char* text, const char* message) { + char buffer[128]; + std::memset(buffer, 0x5a, sizeof(buffer)); + Require(i2t_ASN1_OBJECT_nid_postfix(buffer, space, &object) == expected, message); + Require(std::strcmp(buffer, text) == 0, message); + Require(static_cast<unsigned char>(buffer[std::strlen(text) + 1]) == 0x5a || std::strlen(text) + 1 >= static_cast<std::size_t>(space), + "nothing is written after the terminator"); +} + +void Table() { + const auto& undefined = LibreSslObjects_nid_no_patch[0]; + Require(std::strcmp(undefined.shortName, "UNDEF") == 0 && std::strcmp(undefined.longName, "undefined") == 0 && !undefined.data, "NID 0"); + const auto& commonName = LibreSslObjects_nid_no_patch[13]; + Require(commonName.nid == 13 && std::strcmp(commonName.shortName, "CN") == 0 && std::strcmp(commonName.longName, "commonName") == 0 + && commonName.length == 3 && std::memcmp(commonName.data, "\x55\x04\x03", 3) == 0 && commonName.flags == 0, "commonName"); + const auto& last = LibreSslObjects_nid_no_patch[LibreSslObjectCount - 1]; + Require(last.nid == 955 && std::strcmp(last.shortName, "Ed448ph") == 0, "the last 2.5.4 NID"); + for (int nid : {102, 118, 350, 511, 772, 774, 775}) { + const auto& gap = LibreSslObjects_nid_no_patch[nid]; + Require(!gap.shortName && !gap.longName && gap.nid == 0 && gap.length == 0 && !gap.data, "unassigned NIDs are empty"); + } + for (int index = 0; index < LibreSslObjectOrderCount; ++index) + Require(LibreSslObjectOrder_nid_no_patch[index] < LibreSslObjectCount, "the search order indexes the table"); +} + +void Lookup() { + Require(LibreSslObjectNid_nid_no_patch(nullptr) == 0, "null object"); + Encoded commonName{0x55, 0x04, 0x03}; + Require(LibreSslObjectNid_nid_no_patch(&commonName.object) == 13, "an encoding found by search"); + Encoded preset{0x55, 0x04, 0x03, 0x01}, unknown{0x2a, 0x03, 0x04}; + preset.object.nid = 6; + Require(LibreSslObjectNid_nid_no_patch(&preset.object) == 6, "a set NID wins without a search"); + Require(LibreSslObjectNid_nid_no_patch(&unknown.object) == 0, "an unknown encoding"); + Encoded organization{0x2b}; + Require(LibreSslObjectNid_nid_no_patch(&organization.object) == 379, "1.3 is listed twice; the search lands on ORG as in LibreSSL"); + unsigned char none = 0; + LibreSslAsn1Object empty{nullptr, nullptr, 0, 0, &none, 0}; + Require(LibreSslObjectNid_nid_no_patch(&empty) == 404, "an empty encoding lands on NID_ccitt"); +} + +void Names() { + Encoded commonName{0x55, 0x04, 0x03}; + RequireText(commonName.object, 64, 10, "commonName", "the long name"); + RequireText(commonName.object, 5, 10, "comm", "a short buffer truncates and returns the full length"); + RequireText(commonName.object, 1, 10, "", "a one-byte buffer holds the terminator"); + Require(i2t_ASN1_OBJECT_nid_postfix(nullptr, 0, &commonName.object) == 10, "a zero-length buffer is not written"); + Encoded named{0x2a, 0x03, 0x04}, sha256{0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01}; + named.object.nid = 13; + RequireText(named.object, 64, 10, "commonName", "a set NID names the object whatever its encoding"); + RequireText(sha256.object, 64, 6, "sha256", "a long name equal to the short name"); +} + +void Numbers() { + Encoded unknown{0x2a, 0x03, 0x04}; + RequireText(unknown.object, 64, 7, "1.2.3.4", "an unknown OID is printed in dotted form"); + RequireText(unknown.object, 4, 7, "1.2", "a dotted OID is truncated part by part"); + RequireText(unknown.object, 2, 7, "1", "a two-byte buffer keeps the first arc"); + RequireText(unknown.object, 1, 7, "", "a one-byte buffer keeps nothing"); + Encoded second{0x88, 0x37, 0x05}; + RequireText(second.object, 64, 7, "2.999.5", "a first subidentifier of 80 or more starts with 2"); + Encoded large{0x2a, 0x82, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00}; + RequireText(large.object, 64, 24, "1.2.18446744073709551616", "an arc past 64 bits is printed exactly"); + Encoded largeFirst{0x82, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00}; + RequireText(largeFirst.object, 64, 22, "2.18446744073709551536", "a first subidentifier past 64 bits loses 80"); + Encoded truncated{0x2a, 0x03, 0x83}; + RequireText(truncated.object, 64, 0, "1.2.3", "a truncated subidentifier returns 0 and keeps what was written"); + Encoded high{0x2a, 0x05}; + high.object.nid = 5000; + RequireText(high.object, 64, 5, "1.2.5", "a NID past the table prints the encoding"); + Require(ERR_get_error_nid_postfix() == 0, "a NID past the table reports no error"); +} + +void Errors() { + ERR_clear_error_nid_postfix(); + Encoded gap{0x2a, 0x07}; + gap.object.nid = 102; + RequireText(gap.object, 64, 5, "1.2.7", "an unassigned NID prints the encoding"); + Require(ERR_get_error_nid_postfix() == UnknownNidError && ERR_get_error_nid_postfix() == UnknownNidError + && ERR_get_error_nid_postfix() == 0, "the long and short name lookups each report OBJ_R_UNKNOWN_NID"); + LibreSslAsn1Object none{nullptr, nullptr, 13, 3, nullptr, 0}; + RequireText(none, 64, 0, "", "an object without data"); + Require(i2t_ASN1_OBJECT_nid_postfix(nullptr, 0, nullptr) == 0, "a null object"); + char buffer[8]; + RequireThrows([&] { i2t_ASN1_OBJECT_nid_postfix(buffer, -1, &gap.object); }, "a negative length"); + RequireThrows([&] { i2t_ASN1_OBJECT_nid_postfix(nullptr, 8, &gap.object); }, "a null buffer with a length"); +} + +} // namespace + +int main() { + std::array<void*, 10> api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + Table(); + Lookup(); + Names(); + Numbers(); + Errors(); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 723c8f3d9d..0e02791474 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 319 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 318 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -23,3 +23,5 @@ - [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and unrepresentable signed integer values throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. - [SSL_alert_desc_string_long](../../../core/libs/prx/libSceLibreSsl/Alert.cpp) - Console execution and equivalence across other firmware revisions remain unverified. BunnyFl reports identical strings for 3,841 inputs across three runs against the firmware 13.42 module executed natively on a PC ([comparison](https://github.com/boykopovar/AnyPS5/pull/9870#issuecomment-6406774281)); this is not a console run. The mapping follows LibreSSL 2.5.4. + +- [i2t_ASN1_OBJECT](../../../core/libs/prx/libSceLibreSsl/src/ObjectText.cpp) and the [object table](../../../core/libs/prx/libSceLibreSsl/src/ObjectTable.cpp) are unverified on PS5. The 956 objects and their search order are generated from [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/objects) `objects.txt` and `obj_mac.num` with its `objects.pl` and `obj_dat.pl`, the version string the module holds; LibreSSL 2.7 adds NIDs 956-967, so its `jurisdictionLocalityName`, `jurisdictionStateOrProvinceName` and `jurisdictionCountryName` OIDs print by name there and numerically here. `OBJ_obj2txt` follows 2.5.4 `obj_dat.c`, with objects added at run time left out because no export of this module adds one. A negative buffer length, and a null buffer with a positive length, throw instead of being passed on as a `size_t` From 17693e0e00e41f6df759084fde6178951aee0f9f Mon Sep 17 00:00:00 2001 From: thepoppfish <147332153+thepoppfish@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:50:10 +0200 Subject: [PATCH 394/702] fix(libSceLibreSsl): handle object text allocation failures --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 14 ++ .../prx/libSceLibreSsl/src/ObjectText.cpp | 114 +++++++++------ .../tests/ObjectTextAllocation.cpp | 132 ++++++++++++++++++ 3 files changed, 215 insertions(+), 45 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/tests/ObjectTextAllocation.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index ab5af3ae25..d6a05aeba4 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -193,3 +193,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_libressl_object_allocation_tests + tests/ObjectTextAllocation.cpp src/ObjectText.cpp src/ObjectTable.cpp src/Error.cpp) + target_include_directories(guest_libressl_object_allocation_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_object_allocation_tests PRIVATE libc) + configure_windows_unwind(guest_libressl_object_allocation_tests) + add_test(NAME guest_libressl_object_allocation COMMAND guest_libressl_object_allocation_tests) + set_tests_properties(guest_libressl_object_allocation PROPERTIES TIMEOUT 8) + if(WIN32) + set_tests_properties(guest_libressl_object_allocation PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/src/ObjectText.cpp b/core/libs/prx/libSceLibreSsl/src/ObjectText.cpp index e41bbdd11c..3d954047ac 100644 --- a/core/libs/prx/libSceLibreSsl/src/ObjectText.cpp +++ b/core/libs/prx/libSceLibreSsl/src/ObjectText.cpp @@ -2,11 +2,15 @@ #include "prx/libSceLibreSsl/include/Error.hpp" #include "prx/libc/include/General.hpp" #include <algorithm> +#include <charconv> #include <cstddef> #include <cstdint> #include <cstring> #include <limits> +#include <new> +#include <optional> #include <string> +#include <string_view> #include <vector> static_assert(sizeof(LibreSslAsn1Object) == 40); @@ -19,6 +23,8 @@ static_assert(offsetof(LibreSslAsn1Object, flags) == 32); namespace { constexpr int ObjectLibrary = 8; +constexpr int BigNumberLibrary = 3; +constexpr int AllocationFailure = 65; constexpr int AnyFunction = 0xfff; constexpr int UnknownNid = 101; @@ -92,7 +98,7 @@ const char* Name(int nid, bool longName) { return longName ? object.longName : object.shortName; } -void Write(char*& buffer, int& space, const std::string& text) { +void Write(char*& buffer, int& space, std::string_view text) { if (space > 0) { const std::size_t copied = std::min(text.size(), static_cast<std::size_t>(space - 1)); std::memcpy(buffer, text.data(), copied); @@ -107,6 +113,63 @@ void Write(char*& buffer, int& space, const std::string& text) { } } +int NumericObjectText(char* buffer, int space, const LibreSslAsn1Object* object) { + int result = 0; + int remaining = object->length; + const unsigned char* bytes = object->data; + bool first = true; + while (remaining > 0) { + std::uint64_t value = 0; + bool big = false; + std::optional<BigNumber> number; + for (;;) { + const unsigned char byte = *bytes++; + --remaining; + if (remaining == 0 && (byte & 0x80)) return 0; + if (big) number->Add(byte & 0x7f); + else value |= byte & 0x7f; + if (!(byte & 0x80)) break; + if (!big && value > (std::numeric_limits<std::uint64_t>::max() >> 7)) { + number.emplace(value); + big = true; + } + if (big) number->ShiftLeft7(); + else value <<= 7; + } + if (first) { + first = false; + int arc; + if (value >= 80) { + arc = 2; + if (big) number->Subtract(80); + else value -= 80; + } else { + arc = static_cast<int>(value / 40); + value -= static_cast<std::uint64_t>(arc) * 40; + } + if (space > 1) { + *buffer++ = static_cast<char>('0' + arc); + *buffer = '\0'; + --space; + } + ++result; + } + if (big) { + const std::string text = "." + number->Decimal(); + Write(buffer, space, text); + result += static_cast<int>(text.size()); + } else { + char text[32]; + text[0] = '.'; + const auto converted = std::to_chars(text + 1, text + sizeof(text), value); + const std::string_view part(text, static_cast<std::size_t>(converted.ptr - text)); + Write(buffer, space, part); + result += static_cast<int>(part.size()); + } + } + return result; +} + int ObjectText(char* buffer, int space, const LibreSslAsn1Object* object, const char* function) { if (space < 0 || (space > 0 && !buffer)) NotImplemented_nid_no_patch(function); if (space > 0) buffer[0] = '\0'; @@ -125,51 +188,12 @@ int ObjectText(char* buffer, int space, const LibreSslAsn1Object* object, const return static_cast<int>(length); } } - int result = 0; - int remaining = object->length; - const unsigned char* bytes = object->data; - bool first = true; - while (remaining > 0) { - std::uint64_t value = 0; - bool big = false; - BigNumber number(0); - for (;;) { - const unsigned char byte = *bytes++; - --remaining; - if (remaining == 0 && (byte & 0x80)) return 0; - if (big) number.Add(byte & 0x7f); - else value |= byte & 0x7f; - if (!(byte & 0x80)) break; - if (!big && value > (std::numeric_limits<std::uint64_t>::max() >> 7)) { - number = BigNumber(value); - big = true; - } - if (big) number.ShiftLeft7(); - else value <<= 7; - } - if (first) { - first = false; - int arc; - if (value >= 80) { - arc = 2; - if (big) number.Subtract(80); - else value -= 80; - } else { - arc = static_cast<int>(value / 40); - value -= static_cast<std::uint64_t>(arc) * 40; - } - if (space > 1) { - *buffer++ = static_cast<char>('0' + arc); - *buffer = '\0'; - --space; - } - ++result; - } - const std::string text = "." + (big ? number.Decimal() : std::to_string(value)); - Write(buffer, space, text); - result += static_cast<int>(text.size()); + try { + return NumericObjectText(buffer, space, object); + } catch (const std::bad_alloc&) { + LibreSslPutError_nid_no_patch(BigNumberLibrary, AnyFunction, AllocationFailure); + return 0; } - return result; } } // namespace diff --git a/core/libs/prx/libSceLibreSsl/tests/ObjectTextAllocation.cpp b/core/libs/prx/libSceLibreSsl/tests/ObjectTextAllocation.cpp new file mode 100644 index 0000000000..36922053d3 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/tests/ObjectTextAllocation.cpp @@ -0,0 +1,132 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libSceLibreSsl/include/Error.hpp" +#include <algorithm> +#include <array> +#include <climits> +#include <cstddef> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <initializer_list> +#include <new> +#include <vector> + +namespace { +thread_local bool armed = false, fired = false; +thread_local std::size_t failAt = 0, allocations = 0; +thread_local std::array<void*, 256> tracked{}; +std::size_t failures = 0, checks = 0, faultCases = 0, escaped = 0; +void Check(bool value, const char* label) { + ++checks; + if (value) return; + ++failures; + if (failures <= 8) std::fprintf(stderr, "FAIL: %s\n", label); +} +std::size_t Live() { + return std::count_if(tracked.begin(), tracked.end(), [](void* p) { return p != nullptr; }); +} +} + +void* operator new(std::size_t size) { + if (armed) { + ++allocations; + if (failAt == allocations) { fired = true; throw std::bad_alloc(); } + } + void* pointer = std::malloc(size ? size : 1); + if (!pointer) throw std::bad_alloc(); + if (armed) { + auto slot = std::find(tracked.begin(), tracked.end(), nullptr); + if (slot == tracked.end()) std::abort(); + *slot = pointer; + } + return pointer; +} +void* operator new[](std::size_t size) { return ::operator new(size); } +void operator delete(void* pointer) noexcept { + if (pointer) { + auto slot = std::find(tracked.begin(), tracked.end(), pointer); + if (slot != tracked.end()) *slot = nullptr; + } + std::free(pointer); +} +void operator delete[](void* pointer) noexcept { ::operator delete(pointer); } +void operator delete(void* pointer, std::size_t) noexcept { ::operator delete(pointer); } +void operator delete[](void* pointer, std::size_t) noexcept { ::operator delete(pointer); } + +struct NumericRun { + std::array<char, 512> buffer; + int returned; + bool threw, allocationFailed; + std::size_t calls, live; + std::uint64_t error, following; +}; +NumericRun Run(const unsigned char* data, int length, int space, std::size_t fail, bool seed = false) { + NumericRun result{}; + result.buffer.fill('Z'); + LibreSslAsn1Object object{nullptr, nullptr, 0, length, data, 0}; + ERR_clear_error_nid_postfix(); + if (seed) LibreSslPutError_nid_no_patch(8, 0xfff, 101); + allocations = 0; failAt = fail; fired = false; + armed = true; + try { result.returned = i2t_ASN1_OBJECT_nid_postfix(result.buffer.data(), space, &object); } + catch (const std::bad_alloc&) { result.threw = true; } + armed = false; + result.calls = allocations; + result.allocationFailed = fired; + result.live = Live(); + result.error = ERR_get_error_nid_postfix(); + result.following = ERR_get_error_nid_postfix(); + return result; +} + +int main() { + constexpr std::uint64_t allocationError = (3ull << 24) | (0xfffull << 12) | 65; + std::size_t fastCases = 0; + const unsigned char shortOid[] {0x2a, 0x03, 0x04}; + const unsigned char maxWord[] {0x81,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}; + const unsigned char malformed[] {0x2a,0x81}; + for (const auto bytes : {std::pair{shortOid, sizeof(shortOid)}, std::pair{maxWord, sizeof(maxWord)}, std::pair{malformed, sizeof(malformed)}}) { + for (int space : {0, 1, 2, 5, 32, 512}) { + const auto expected = Run(bytes.first, static_cast<int>(bytes.second), space, 0); + const auto result = Run(bytes.first, static_cast<int>(bytes.second), space, 1); + ++fastCases; + Check(!result.threw && result.returned == expected.returned && result.buffer == expected.buffer, "word/malformed path works when host new would fail"); + Check(result.calls == 0 && !result.allocationFailed, "word/malformed path does not allocate"); + Check(result.live == 0 && result.error == 0, "word/malformed path preserves empty error queue"); + escaped += result.threw; + } + } + for (int size : {11, 20, 40}) { + for (bool later : {false, true}) { + std::vector<unsigned char> bytes; + if (later) bytes = {0x2a, 0x03}; + bytes.insert(bytes.end(), static_cast<std::size_t>(size), 0xff); + bytes.back() = 0x7f; + for (int space : {0, 1, 5, 512}) { + const auto normal = Run(bytes.data(), static_cast<int>(bytes.size()), space, 0); + for (std::size_t point = 1; point <= normal.calls; ++point) { + const auto result = Run(bytes.data(), static_cast<int>(bytes.size()), space, point); + ++faultCases; + escaped += result.threw; + Check(result.allocationFailed, "selected big-number allocation actually failed"); + Check(!result.threw && result.returned == 0, "big-number allocation failure returns zero"); + Check(result.error == allocationError && result.following == 0, "big-number allocation failure uses BN malloc error"); + Check(result.live == 0, "failed conversion releases scratch allocations"); + if (space > 0) { + const void* end = std::memchr(result.buffer.data(), 0, static_cast<std::size_t>(space)); + Check(end != nullptr, "failed conversion leaves output terminated"); + } + } + } + } + } + const unsigned char big[] {0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}; + const auto seeded = Run(big, sizeof(big), 64, 1, true); + escaped += seeded.threw; + Check(!seeded.threw && seeded.returned == 0 && seeded.error == ((8ull << 24) | (0xfffull << 12) | 101) && seeded.following == allocationError, + "allocation error appends to existing queue entries"); + Check(ERR_get_error_nid_postfix() == 0 && seeded.live == 0, "seeded failure adds exactly one error and leaks nothing"); + std::printf("{\"fast_cases\":%zu,\"allocation_fault_cases\":%zu,\"checks\":%zu,\"failures\":%zu,\"escaped\":%zu,\"final_live\":%zu}\n", + fastCases, faultCases, checks, failures, escaped, Live()); + return failures ? 1 : 0; +} From 397b8d6a1b3612001dcee8752a9fc953270552bd Mon Sep 17 00:00:00 2001 From: JaceTheBlu <xngt974@gmail.com> Date: Mon, 26 Oct 2026 08:04:02 +0000 Subject: [PATCH 395/702] test(libSceLibreSsl): prime the unwinder before counting object text blocks On Linux the first exception the allocation test throws goes through libc's unwinder, which caches two frame descriptions with operator new for the rest of the process. The test counted them as blocks leaked by i2t_ASN1_OBJECT and failed every fault case. A first failing conversion now runs before the counted cases, and the blocks it leaves are not tracked. Against the head before the allocation fix the test still reports the escaped exceptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceLibreSsl/tests/ObjectTextAllocation.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceLibreSsl/tests/ObjectTextAllocation.cpp b/core/libs/prx/libSceLibreSsl/tests/ObjectTextAllocation.cpp index 36922053d3..d18cd76455 100644 --- a/core/libs/prx/libSceLibreSsl/tests/ObjectTextAllocation.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/ObjectTextAllocation.cpp @@ -82,6 +82,9 @@ NumericRun Run(const unsigned char* data, int length, int space, std::size_t fai int main() { constexpr std::uint64_t allocationError = (3ull << 24) | (0xfffull << 12) | 65; std::size_t fastCases = 0; + const unsigned char warmUp[] {0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}; + Run(warmUp, sizeof(warmUp), 0, 1); + tracked.fill(nullptr); const unsigned char shortOid[] {0x2a, 0x03, 0x04}; const unsigned char maxWord[] {0x81,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}; const unsigned char malformed[] {0x2a,0x81}; From f407d1bd2b51fb5f38b2184b41d2df9dbef1e5bd Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 20:32:39 +1300 Subject: [PATCH 396/702] fix(libSceIcu): drop the stubs open PRs implement Remove the 36 stubs whose NIDs open PRs #10962-#10967, #11115, #11118, #11122, #11132 and #11136 add to libSceIcu, leaving 112. --- core/libs/prx/libSceIcu/Export.cpp | 180 ----------------------------- docs/dev/debt/libSceIcu.md | 2 +- 2 files changed, 1 insertion(+), 181 deletions(-) diff --git a/core/libs/prx/libSceIcu/Export.cpp b/core/libs/prx/libSceIcu/Export.cpp index 12dab64687..cef6411bce 100644 --- a/core/libs/prx/libSceIcu/Export.cpp +++ b/core/libs/prx/libSceIcu/Export.cpp @@ -186,11 +186,6 @@ int APS5_VABI sceIcuUnknown_1SsridDRBYQ_nid_no_patch() { } APS5_EXPORT("1SsridDRBYQ", sceIcuUnknown_1SsridDRBYQ_nid_no_patch); -int APS5_VABI sceIcuUnknown_71sgC7_39is_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("71sgC7+39is", sceIcuUnknown_71sgC7_39is_nid_no_patch); - int APS5_VABI sceIcuUnknown_a25spvWYEOA_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -206,11 +201,6 @@ int APS5_VABI sceIcuUnknown_wybPkXuWnoE_nid_no_patch() { } APS5_EXPORT("wybPkXuWnoE", sceIcuUnknown_wybPkXuWnoE_nid_no_patch); -int APS5_VABI sceIcuUnknown_cY8dSl6Rqr4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("cY8dSl6Rqr4", sceIcuUnknown_cY8dSl6Rqr4_nid_no_patch); - int APS5_VABI sceIcuUnknown_2qAOcgRJZ3M_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -316,11 +306,6 @@ int APS5_VABI sceIcuUnknown_zGuhKwXM9I0_nid_no_patch() { } APS5_EXPORT("zGuhKwXM9I0", sceIcuUnknown_zGuhKwXM9I0_nid_no_patch); -int APS5_VABI sceIcuUnknown_iRm8tkU1ir4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("iRm8tkU1ir4", sceIcuUnknown_iRm8tkU1ir4_nid_no_patch); - int APS5_VABI sceIcuUnknown_6y_NITWLmmE_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -471,51 +456,6 @@ int APS5_VABI sceIcuUnknown_CfP4Q2Yjm1U_nid_no_patch() { } APS5_EXPORT("CfP4Q2Yjm1U", sceIcuUnknown_CfP4Q2Yjm1U_nid_no_patch); -int APS5_VABI sceIcuUnknown_5UagplYjXBo_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("5UagplYjXBo", sceIcuUnknown_5UagplYjXBo_nid_no_patch); - -int APS5_VABI sceIcuUnknown_pkGTJH2IstM_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("pkGTJH2IstM", sceIcuUnknown_pkGTJH2IstM_nid_no_patch); - -int APS5_VABI sceIcuUnknown_zx0jGj_nSIM_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("zx0jGj+nSIM", sceIcuUnknown_zx0jGj_nSIM_nid_no_patch); - -int APS5_VABI sceIcuUnknown_b5KdXXbihxE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("b5KdXXbihxE", sceIcuUnknown_b5KdXXbihxE_nid_no_patch); - -int APS5_VABI sceIcuUnknown_xHVUdb5NNSw_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("xHVUdb5NNSw", sceIcuUnknown_xHVUdb5NNSw_nid_no_patch); - -int APS5_VABI sceIcuUnknown_uAMs0e65nCk_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("uAMs0e65nCk", sceIcuUnknown_uAMs0e65nCk_nid_no_patch); - -int APS5_VABI sceIcuUnknown_zqQWBy7O_00_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("zqQWBy7O-00", sceIcuUnknown_zqQWBy7O_00_nid_no_patch); - -int APS5_VABI sceIcuUnknown_OEiVDwLURmI_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("OEiVDwLURmI", sceIcuUnknown_OEiVDwLURmI_nid_no_patch); - -int APS5_VABI sceIcuUnknown_ojLjExhl_hE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("ojLjExhl-hE", sceIcuUnknown_ojLjExhl_hE_nid_no_patch); - int APS5_VABI sceIcuUnknown_sQ0avkeBLo4_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -561,16 +501,6 @@ int APS5_VABI sceIcuUnknown_TNZrG2iXikw_nid_no_patch() { } APS5_EXPORT("TNZrG2iXikw", sceIcuUnknown_TNZrG2iXikw_nid_no_patch); -int APS5_VABI sceIcuUnknown__t8PVjghGo4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("-t8PVjghGo4", sceIcuUnknown__t8PVjghGo4_nid_no_patch); - -int APS5_VABI sceIcuUnknown_HK0gjeXo2eo_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("HK0gjeXo2eo", sceIcuUnknown_HK0gjeXo2eo_nid_no_patch); - int APS5_VABI sceIcuUnknown_Z5ljxAg3thw_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -591,26 +521,6 @@ int APS5_VABI sceIcuUnknown_q3mX36AQeC4_nid_no_patch() { } APS5_EXPORT("q3mX36AQeC4", sceIcuUnknown_q3mX36AQeC4_nid_no_patch); -int APS5_VABI sceIcuUnknown_8XjRlb2loGU_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("8XjRlb2loGU", sceIcuUnknown_8XjRlb2loGU_nid_no_patch); - -int APS5_VABI sceIcuUnknown_kfOVuKHkkV4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("kfOVuKHkkV4", sceIcuUnknown_kfOVuKHkkV4_nid_no_patch); - -int APS5_VABI sceIcuUnknown_2AqqzZ9fKwY_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("2AqqzZ9fKwY", sceIcuUnknown_2AqqzZ9fKwY_nid_no_patch); - -int APS5_VABI sceIcuUnknown_4h4kYUxRDgU_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("4h4kYUxRDgU", sceIcuUnknown_4h4kYUxRDgU_nid_no_patch); - int APS5_VABI sceIcuUnknown_RIcNJWDYKhM_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -631,16 +541,6 @@ int APS5_VABI sceIcuUnknown_X7gbPHVoNEw_nid_no_patch() { } APS5_EXPORT("X7gbPHVoNEw", sceIcuUnknown_X7gbPHVoNEw_nid_no_patch); -int APS5_VABI sceIcuUnknown_23uLv15jvFA_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("23uLv15jvFA", sceIcuUnknown_23uLv15jvFA_nid_no_patch); - -int APS5_VABI sceIcuUnknown_MSr9DE8l0Sc_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("MSr9DE8l0Sc", sceIcuUnknown_MSr9DE8l0Sc_nid_no_patch); - int APS5_VABI sceIcuUnknown_aXUj3BT58vY_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -651,96 +551,16 @@ int APS5_VABI sceIcuUnknown_FCx2DacLDWs_nid_no_patch() { } APS5_EXPORT("FCx2DacLDWs", sceIcuUnknown_FCx2DacLDWs_nid_no_patch); -int APS5_VABI sceIcuUnknown_W7SNhSZ_YdI_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("W7SNhSZ-YdI", sceIcuUnknown_W7SNhSZ_YdI_nid_no_patch); - -int APS5_VABI sceIcuUnknown_4g6BoFF_ywE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("4g6BoFF-ywE", sceIcuUnknown_4g6BoFF_ywE_nid_no_patch); - -int APS5_VABI sceIcuUnknown_vRW6XDP_8J4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("vRW6XDP-8J4", sceIcuUnknown_vRW6XDP_8J4_nid_no_patch); - -int APS5_VABI sceIcuUnknown_qhWR6O_Zp_U_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("qhWR6O+Zp+U", sceIcuUnknown_qhWR6O_Zp_U_nid_no_patch); - -int APS5_VABI sceIcuUnknown_Zkuj3yHaXQQ_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("Zkuj3yHaXQQ", sceIcuUnknown_Zkuj3yHaXQQ_nid_no_patch); - -int APS5_VABI sceIcuUnknown_WScz3_NnifQ_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("WScz3+NnifQ", sceIcuUnknown_WScz3_NnifQ_nid_no_patch); - -int APS5_VABI sceIcuUnknown_rjnCmUev3aU_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("rjnCmUev3aU", sceIcuUnknown_rjnCmUev3aU_nid_no_patch); - -int APS5_VABI sceIcuUnknown_2vIP9cBK1R0_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("2vIP9cBK1R0", sceIcuUnknown_2vIP9cBK1R0_nid_no_patch); - int APS5_VABI sceIcuUnknown_ekxUEjhuk3Q_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("ekxUEjhuk3Q", sceIcuUnknown_ekxUEjhuk3Q_nid_no_patch); -int APS5_VABI sceIcuUnknown_JwGa8TaETwI_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("JwGa8TaETwI", sceIcuUnknown_JwGa8TaETwI_nid_no_patch); - -int APS5_VABI sceIcuUnknown_H4haJKBliZE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("H4haJKBliZE", sceIcuUnknown_H4haJKBliZE_nid_no_patch); - -int APS5_VABI sceIcuUnknown_ikElnlIFyWs_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("ikElnlIFyWs", sceIcuUnknown_ikElnlIFyWs_nid_no_patch); - -int APS5_VABI sceIcuUnknown_RR1fhpBoZ7w_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("RR1fhpBoZ7w", sceIcuUnknown_RR1fhpBoZ7w_nid_no_patch); - -int APS5_VABI sceIcuUnknown_yt7hn3QeBvY_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("yt7hn3QeBvY", sceIcuUnknown_yt7hn3QeBvY_nid_no_patch); - -int APS5_VABI sceIcuUnknown_i4FsTbO__x8_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("i4FsTbO-+x8", sceIcuUnknown_i4FsTbO__x8_nid_no_patch); - -int APS5_VABI sceIcuUnknown_ebmFXXx04K4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("ebmFXXx04K4", sceIcuUnknown_ebmFXXx04K4_nid_no_patch); - int APS5_VABI sceIcuUnknown_BsjbIVU27jQ_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("BsjbIVU27jQ", sceIcuUnknown_BsjbIVU27jQ_nid_no_patch); -int APS5_VABI sceIcuUnknown_1YDkzlcc8YY_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("1YDkzlcc8YY", sceIcuUnknown_1YDkzlcc8YY_nid_no_patch); - int APS5_VABI sceIcuUnknown_wezXBwKdjAY_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index b623b33546..83fe801cc4 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -6,4 +6,4 @@ ### Known export names; ABI prototypes unverified -- The 148 `sceIcuUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceIcu/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 112 `sceIcuUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceIcu/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From 1affd8f73128b0e944ad7c0ef791a8e991d18edf Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 20:33:45 +1300 Subject: [PATCH 397/702] fix(libSceFreeTypeOl): drop the exports open pull requests already implement Remove 32 stubs whose NIDs are implemented by other open pull requests (FreeType functions in #9398 #9399 #9404 #9410 #9465 #9588 #9792 #9924 #9933 #10579 #10588 #10591 #10600 #10601 #10602 #10606 #10613 #10616 #10897) and correct the count in the debt entry. --- core/libs/prx/libSceFreeTypeOl/Export.cpp | 160 ---------------------- docs/dev/debt/libSceFreeTypeOl.md | 2 +- 2 files changed, 1 insertion(+), 161 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOl/Export.cpp b/core/libs/prx/libSceFreeTypeOl/Export.cpp index e01fa51b09..68dd03089b 100644 --- a/core/libs/prx/libSceFreeTypeOl/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOl/Export.cpp @@ -420,11 +420,6 @@ int APS5_VABI sceFreeTypeOlUnknown_Ns9a_LjOcDw_nid_no_patch() { } APS5_EXPORT("Ns9a-LjOcDw", sceFreeTypeOlUnknown_Ns9a_LjOcDw_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_pSgOzwkzx4Y_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("pSgOzwkzx4Y", sceFreeTypeOlUnknown_pSgOzwkzx4Y_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_LonHHN98tMI_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -435,26 +430,11 @@ int APS5_VABI sceFreeTypeOlUnknown_Uw_BZJpflxk_nid_no_patch() { } APS5_EXPORT("Uw+BZJpflxk", sceFreeTypeOlUnknown_Uw_BZJpflxk_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_p90G4PB_rPc_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("p90G4PB-rPc", sceFreeTypeOlUnknown_p90G4PB_rPc_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_21vkVuOP2lk_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("21vkVuOP2lk", sceFreeTypeOlUnknown_21vkVuOP2lk_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_ZqvTX8u1bEc_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("ZqvTX8u1bEc", sceFreeTypeOlUnknown_ZqvTX8u1bEc_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_wcMdf_kMx7o_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("wcMdf-kMx7o", sceFreeTypeOlUnknown_wcMdf_kMx7o_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_h0n4Fb9j2R8_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -465,164 +445,24 @@ int APS5_VABI sceFreeTypeOlUnknown_wLqQQWyOdpc_nid_no_patch() { } APS5_EXPORT("wLqQQWyOdpc", sceFreeTypeOlUnknown_wLqQQWyOdpc_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_92e4jZiW9Tc_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("92e4jZiW9Tc", sceFreeTypeOlUnknown_92e4jZiW9Tc_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_exDiYq9Q1GM_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("exDiYq9Q1GM", sceFreeTypeOlUnknown_exDiYq9Q1GM_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_pO0sA7m_UKY_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("pO0sA7m+UKY", sceFreeTypeOlUnknown_pO0sA7m_UKY_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_fp_jO_c8Rfk_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("fp-jO+c8Rfk", sceFreeTypeOlUnknown_fp_jO_c8Rfk_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_RdrrejUMiJ0_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("RdrrejUMiJ0", sceFreeTypeOlUnknown_RdrrejUMiJ0_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_oJm1lThZ3iQ_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("oJm1lThZ3iQ", sceFreeTypeOlUnknown_oJm1lThZ3iQ_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_1XCz6Gk0Lt4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("1XCz6Gk0Lt4", sceFreeTypeOlUnknown_1XCz6Gk0Lt4_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_UTumVjeIQtg_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("UTumVjeIQtg", sceFreeTypeOlUnknown_UTumVjeIQtg_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown__G9vZcqBAfY_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("+G9vZcqBAfY", sceFreeTypeOlUnknown__G9vZcqBAfY_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_dPcZ76eZb64_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("dPcZ76eZb64", sceFreeTypeOlUnknown_dPcZ76eZb64_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_Vrab2WK1mgk_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("Vrab2WK1mgk", sceFreeTypeOlUnknown_Vrab2WK1mgk_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_tl72PmqhXww_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("tl72PmqhXww", sceFreeTypeOlUnknown_tl72PmqhXww_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_9Lm2mfsSmws_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("9Lm2mfsSmws", sceFreeTypeOlUnknown_9Lm2mfsSmws_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_mGMeRKWGTL4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("mGMeRKWGTL4", sceFreeTypeOlUnknown_mGMeRKWGTL4_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_EruGr0TR1CU_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("EruGr0TR1CU", sceFreeTypeOlUnknown_EruGr0TR1CU_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_EqN_ED6P2FA_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("EqN+ED6P2FA", sceFreeTypeOlUnknown_EqN_ED6P2FA_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_fHMsRluEMfI_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("fHMsRluEMfI", sceFreeTypeOlUnknown_fHMsRluEMfI_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_dMSDtQsYuJM_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("dMSDtQsYuJM", sceFreeTypeOlUnknown_dMSDtQsYuJM_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_l55oKq6zmrA_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("l55oKq6zmrA", sceFreeTypeOlUnknown_l55oKq6zmrA_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_eMKHL_Cj50k_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("eMKHL+Cj50k", sceFreeTypeOlUnknown_eMKHL_Cj50k_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_qJFh32pZAJQ_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("qJFh32pZAJQ", sceFreeTypeOlUnknown_qJFh32pZAJQ_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_swrIPzOxcmM_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("swrIPzOxcmM", sceFreeTypeOlUnknown_swrIPzOxcmM_nid_no_patch); - int APS5_VABI sceFreeTypeOlUnknown_5NYsP564wwY_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("5NYsP564wwY", sceFreeTypeOlUnknown_5NYsP564wwY_nid_no_patch); -int APS5_VABI sceFreeTypeOlUnknown_pORpGtdlfqQ_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("pORpGtdlfqQ", sceFreeTypeOlUnknown_pORpGtdlfqQ_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_JQyLCevN3aM_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("JQyLCevN3aM", sceFreeTypeOlUnknown_JQyLCevN3aM_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown__yNEa4OFH1M_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("+yNEa4OFH1M", sceFreeTypeOlUnknown__yNEa4OFH1M_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_Ik17GQynFpw_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("Ik17GQynFpw", sceFreeTypeOlUnknown_Ik17GQynFpw_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_1wFNGIk3FR0_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("1wFNGIk3FR0", sceFreeTypeOlUnknown_1wFNGIk3FR0_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_omDz0x8lyHk_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("omDz0x8lyHk", sceFreeTypeOlUnknown_omDz0x8lyHk_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_vOhW2Wdz0t4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("vOhW2Wdz0t4", sceFreeTypeOlUnknown_vOhW2Wdz0t4_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_e2KoMPOAdZg_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("e2KoMPOAdZg", sceFreeTypeOlUnknown_e2KoMPOAdZg_nid_no_patch); - -int APS5_VABI sceFreeTypeOlUnknown_eSFTUAm1wf8_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("eSFTUAm1wf8", sceFreeTypeOlUnknown_eSFTUAm1wf8_nid_no_patch); - } diff --git a/docs/dev/debt/libSceFreeTypeOl.md b/docs/dev/debt/libSceFreeTypeOl.md index 97d4272c82..7de76d6f2e 100644 --- a/docs/dev/debt/libSceFreeTypeOl.md +++ b/docs/dev/debt/libSceFreeTypeOl.md @@ -2,4 +2,4 @@ ### Known export names; ABI prototypes unverified -- The 124 `sceFreeTypeOlUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOl/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 92 `sceFreeTypeOlUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOl/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From 3383555532b9760f492f65595d4bd238ff91ef81 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 07:36:48 +0000 Subject: [PATCH 398/702] test(agc): check the packets sceAgcDcbGetLodStats writes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/CMakeLists.txt | 13 +++++++ core/libs/prx/libSceAgc/tests/LodStats.cpp | 43 ++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 core/libs/prx/libSceAgc/tests/LodStats.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 82331aea05..7477ef8a89 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -320,6 +320,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_lod_stats_tests EXCLUDE_FROM_ALL tests/LodStats.cpp) +target_include_directories(agc_lod_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_lod_stats_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_lod_stats_tests) +if(BUILD_TESTING) + set_target_properties(agc_lod_stats_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_lod_stats COMMAND agc_lod_stats_tests) + if(WIN32) + set_tests_properties(agc_lod_stats PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() + add_test_executable(agc_interpolant_mapping_tests EXCLUDE_FROM_ALL tests/InterpolantMapping.cpp Shader/src/InterpolantMapping.cpp diff --git a/core/libs/prx/libSceAgc/tests/LodStats.cpp b/core/libs/prx/libSceAgc/tests/LodStats.cpp new file mode 100644 index 0000000000..78994aa2ca --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/LodStats.cpp @@ -0,0 +1,43 @@ +#include "prx/libSceAgc/DcbDraw/include/Stats.hpp" +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstdio> + +namespace { + +struct Storage { + std::array<std::uint32_t, 64> words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; +}; + +int fail(const char* message) { + std::fprintf(stderr, "%s\n", message); + return 1; +} + +} + +int main() { + alignas(64) static std::array<std::uint8_t, 0x840> report{}; + const auto address = reinterpret_cast<std::uintptr_t>(report.data()); + for (const std::uint8_t policy : {0u, 2u}) { + Storage dcb; + const auto* first = sceAgcDcbGetLodStats(&dcb.buffer, policy, nullptr, 0, 0, 0, 0, 0); + const auto* second = sceAgcDcbGetLodStats(&dcb.buffer, policy, report.data(), 0x840, 0, 0, 1, 0); + if (first != dcb.words.data() || second != dcb.words.data() + 5 || dcb.buffer.cursor_up != dcb.words.data() + 10) return fail("LOD stats packets are not five words each"); + const std::array<std::uint32_t, 5> expectedFirst{0xc0031000u, 0, 0, 0, static_cast<std::uint32_t>(policy) << 28u}; + const std::array<std::uint32_t, 5> expectedSecond{0xc0031000u, 0x840u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), (static_cast<std::uint32_t>(policy) << 28u) | (1u << 19u)}; + for (std::size_t index = 0; index < 5; ++index) { + if (dcb.words[index] != expectedFirst[index]) return fail("the sizing LOD stats packet differs"); + if (dcb.words[5 + index] != expectedSecond[index]) return fail("the reporting LOD stats packet differs"); + } + Storage fields; + sceAgcDcbGetLodStats(&fields.buffer, 3, report.data(), 0x40, 0xff, 1, 0, 0xff); + if (fields.words[4] != ((3u << 28u) | (1u << 18u) | (0xffu << 10u) | (0xffu << 2u))) return fail("the LOD stats control word lost a field"); + } + std::puts("AGC LOD stats tests passed"); + return 0; +} From 86cdfb864753abe0ec3ff673fc19766b1336bbcf Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:00:09 +0100 Subject: [PATCH 399/702] feat(libkernel): thread-local storage for Mach-O guests FreeBSD code reaches its thread pointer through %fs and macOS cannot set the fs base. The macOS relinker turns each %fs access of a guest image into a stub that reads the thread pointer from a pthread key and calls Aps5GuestTlsAllocate on a thread's first access, registers the TLS descriptor of an image with Aps5GuestTlsRegister, and routes __tls_get_addr to Aps5GuestTlsGetAddr. No library provided them, so a relinked executable could not start. - Aps5GuestTlsRegister validates the descriptor the relinker wrote and creates the key; its destructor frees the block of a thread. - Aps5GuestTlsAllocateThreadPointer allocates the block (the TLS template followed by zeros, then a 0x100-byte thread control block whose first word points at itself, as on FreeBSD) and stores the thread pointer under the key. - Aps5GuestTlsAllocate is the entry the stubs call: rax holds the descriptor and returns the thread pointer, every other register is preserved, the upper halves of the ymm registers only where AVX exists. - Aps5GuestTlsGetAddr returns the block of the thread plus the offset. Co-authored-by: mugurc <mceker@gmail.com> --- core/libs/prx/libkernel/CMakeLists.txt | 1 + .../libkernel/Pthread/src/GuestTlsMacOs.cpp | 180 ++++++++++++++++++ docs/dev/debt/libkernel.md | 3 +- 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 974df3b88c..a38fb905e4 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -33,6 +33,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Attr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Thread.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Tsd.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/GuestTlsMacOs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Sem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/MainThreadMacOs.cpp diff --git a/core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp b/core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp new file mode 100644 index 0000000000..a4570c338e --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp @@ -0,0 +1,180 @@ +#ifdef __APPLE__ + +#include <pthread.h> +#include <algorithm> +#include <bit> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> + +namespace { + +struct GuestTlsDescriptor { + std::uint64_t version; + std::uint64_t templateAddress; + std::uint64_t templateSize; + std::uint64_t blockSize; + std::uint64_t alignment; + std::uint64_t key; +}; + +struct TlsIndex { + const GuestTlsDescriptor* module; + std::uint64_t offset; +}; + +constexpr std::size_t ThreadControlBlockBytes = 0x100; +constexpr std::size_t AllocationSlot = ThreadControlBlockBytes - sizeof(void*); + +[[noreturn]] void Fail(const char* reason) { + std::fprintf(stderr, "guest TLS: %s\n", reason); + std::abort(); +} + +void Release(void* threadPointer) { + std::free(*reinterpret_cast<void**>(static_cast<std::uint8_t*>(threadPointer) + AllocationSlot)); +} + +} + +extern "C" { + +std::uint8_t Aps5GuestTlsAvx_nid_no_patch = __builtin_cpu_supports("avx") ? 1 : 0; + +void Aps5GuestTlsRegister_nid_no_patch(GuestTlsDescriptor* descriptor) { + if (descriptor == nullptr || descriptor->version != 1) Fail("unknown TLS descriptor"); + if (!std::has_single_bit(descriptor->alignment) || descriptor->blockSize % descriptor->alignment != 0 || descriptor->templateSize > descriptor->blockSize) + Fail("invalid TLS descriptor layout"); + pthread_key_t key; + if (pthread_key_create(&key, Release) != 0) Fail("cannot create a thread pointer key"); + descriptor->key = key; +} + +void* Aps5GuestTlsAllocateThreadPointer_nid_no_patch(const GuestTlsDescriptor* descriptor) { + if (descriptor == nullptr || descriptor->version != 1) Fail("unknown TLS descriptor"); + void* base = nullptr; + const auto alignment = std::max<std::size_t>(descriptor->alignment, 16); + if (posix_memalign(&base, alignment, descriptor->blockSize + ThreadControlBlockBytes) != 0) Fail("cannot allocate a TLS block"); + auto* bytes = static_cast<std::uint8_t*>(base); + if (descriptor->templateSize != 0) + std::memcpy(bytes, reinterpret_cast<const void*>(descriptor->templateAddress), descriptor->templateSize); + std::memset(bytes + descriptor->templateSize, 0, descriptor->blockSize - descriptor->templateSize + ThreadControlBlockBytes); + auto* threadPointer = bytes + descriptor->blockSize; + *reinterpret_cast<void**>(threadPointer) = threadPointer; + *reinterpret_cast<void**>(threadPointer + AllocationSlot) = base; + if (pthread_setspecific(static_cast<pthread_key_t>(descriptor->key), threadPointer) != 0) Fail("cannot store the thread pointer"); + return threadPointer; +} + +void* Aps5GuestTlsGetAddr_nid_no_patch(const TlsIndex* index) { + const auto* descriptor = index->module; + auto* threadPointer = static_cast<std::uint8_t*>(pthread_getspecific(static_cast<pthread_key_t>(descriptor->key))); + if (threadPointer == nullptr) threadPointer = static_cast<std::uint8_t*>(Aps5GuestTlsAllocateThreadPointer_nid_no_patch(descriptor)); + return threadPointer - descriptor->blockSize + index->offset; +} + +} + +asm(R"( + .text + .globl _Aps5GuestTlsAllocate_nid_no_patch +_Aps5GuestTlsAllocate_nid_no_patch: + push %rbp + mov %rsp, %rbp + push %rcx + push %rdx + push %rsi + push %rdi + push %r8 + push %r9 + push %r10 + push %r11 + and $-64, %rsp + sub $512, %rsp + mov %rax, %rdi + testb $1, _Aps5GuestTlsAvx_nid_no_patch(%rip) + jz 1f + vmovdqu %ymm0, 0(%rsp) + vmovdqu %ymm1, 32(%rsp) + vmovdqu %ymm2, 64(%rsp) + vmovdqu %ymm3, 96(%rsp) + vmovdqu %ymm4, 128(%rsp) + vmovdqu %ymm5, 160(%rsp) + vmovdqu %ymm6, 192(%rsp) + vmovdqu %ymm7, 224(%rsp) + vmovdqu %ymm8, 256(%rsp) + vmovdqu %ymm9, 288(%rsp) + vmovdqu %ymm10, 320(%rsp) + vmovdqu %ymm11, 352(%rsp) + vmovdqu %ymm12, 384(%rsp) + vmovdqu %ymm13, 416(%rsp) + vmovdqu %ymm14, 448(%rsp) + vmovdqu %ymm15, 480(%rsp) + call _Aps5GuestTlsAllocateThreadPointer_nid_no_patch + vmovdqu 0(%rsp), %ymm0 + vmovdqu 32(%rsp), %ymm1 + vmovdqu 64(%rsp), %ymm2 + vmovdqu 96(%rsp), %ymm3 + vmovdqu 128(%rsp), %ymm4 + vmovdqu 160(%rsp), %ymm5 + vmovdqu 192(%rsp), %ymm6 + vmovdqu 224(%rsp), %ymm7 + vmovdqu 256(%rsp), %ymm8 + vmovdqu 288(%rsp), %ymm9 + vmovdqu 320(%rsp), %ymm10 + vmovdqu 352(%rsp), %ymm11 + vmovdqu 384(%rsp), %ymm12 + vmovdqu 416(%rsp), %ymm13 + vmovdqu 448(%rsp), %ymm14 + vmovdqu 480(%rsp), %ymm15 + jmp 2f +1: + movdqu %xmm0, 0(%rsp) + movdqu %xmm1, 32(%rsp) + movdqu %xmm2, 64(%rsp) + movdqu %xmm3, 96(%rsp) + movdqu %xmm4, 128(%rsp) + movdqu %xmm5, 160(%rsp) + movdqu %xmm6, 192(%rsp) + movdqu %xmm7, 224(%rsp) + movdqu %xmm8, 256(%rsp) + movdqu %xmm9, 288(%rsp) + movdqu %xmm10, 320(%rsp) + movdqu %xmm11, 352(%rsp) + movdqu %xmm12, 384(%rsp) + movdqu %xmm13, 416(%rsp) + movdqu %xmm14, 448(%rsp) + movdqu %xmm15, 480(%rsp) + call _Aps5GuestTlsAllocateThreadPointer_nid_no_patch + movdqu 0(%rsp), %xmm0 + movdqu 32(%rsp), %xmm1 + movdqu 64(%rsp), %xmm2 + movdqu 96(%rsp), %xmm3 + movdqu 128(%rsp), %xmm4 + movdqu 160(%rsp), %xmm5 + movdqu 192(%rsp), %xmm6 + movdqu 224(%rsp), %xmm7 + movdqu 256(%rsp), %xmm8 + movdqu 288(%rsp), %xmm9 + movdqu 320(%rsp), %xmm10 + movdqu 352(%rsp), %xmm11 + movdqu 384(%rsp), %xmm12 + movdqu 416(%rsp), %xmm13 + movdqu 448(%rsp), %xmm14 + movdqu 480(%rsp), %xmm15 +2: + lea -64(%rbp), %rsp + pop %r11 + pop %r10 + pop %r9 + pop %r8 + pop %rdi + pop %rsi + pop %rdx + pop %rcx + pop %rbp + ret +)"); + +#endif diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index bf0ae0d386..67136df950 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -137,8 +137,9 @@ ### Functional - [scePthreadYield](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) on Windows calls Sleep(1) after 512 consecutive yields that find no other thread ready on the current processor and arrive less than ~10,000 TSC ticks apart (so the span varies with host CPU TSC rate); FreeBSD sched_yield never sleeps. -- [__tls_get_addr](../../../core/libs/prx/libkernel/Module/src/Tls.cpp) (libkernel, NID `vNe1w4diLCs`) throws on macOS, where the relinker binds guest modules to `Aps5GuestTlsGetAddr_nid_no_patch`, which no library defines yet; it also throws for a null index, module 0 and a Windows thread without a block for the module. It reads the module word as the relinker writes it for `R_X86_64_DTPMOD64`: on Windows the address of the module's PE `_tls_index`, resolved through the thread's `ThreadLocalStoragePointer` as in [WindowsGuestStartup.cpp](../../../core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp); on Linux the glibc module ID, passed to glibc's `__tls_get_addr`. Guest modules' imports are bound to the host resolver, so this export is reached only through a lookup by NID. +- [__tls_get_addr](../../../core/libs/prx/libkernel/Module/src/Tls.cpp) (libkernel, NID `vNe1w4diLCs`) throws on macOS, where the relinker binds guest modules to `Aps5GuestTlsGetAddr_nid_no_patch` ([GuestTlsMacOs.cpp](../../../core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp)) instead; it also throws for a null index, module 0 and a Windows thread without a block for the module. It reads the module word as the relinker writes it for `R_X86_64_DTPMOD64`: on Windows the address of the module's PE `_tls_index`, resolved through the thread's `ThreadLocalStoragePointer` as in [WindowsGuestStartup.cpp](../../../core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp); on Linux the glibc module ID, passed to glibc's `__tls_get_addr`. Guest modules' imports are bound to the host resolver, so this export is reached only through a lookup by NID. - [sceKernelGetTscFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) pins its thread to the CPU it runs on for the 20 ms measurement, so that both TSC reads come from one counter. If the pin can't be set (`sched_getcpu` or `pthread_getaffinity_np` fails on Linux, `SetThreadAffinityMask` fails on Windows), the measurement runs unpinned. On a host whose CPU TSCs differ, the result can then be wrong as before. Neither call is known to fail for the calling thread's own CPU, which is always in its allowed set. macOS never pins. +- [Guest TLS on macOS](../../../core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp) (libkernel) - every guest image has its own TLS block and thread pointer, instead of one thread pointer per thread as on the console, and the word at 0x28 of the thread pointer, which FreeBSD code reads as the stack guard, is 0. macOS cannot set the `%fs` base, so the relinker ([MacOsImage.cpp](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)) turns each `%fs` access into a stub that reads the thread pointer from the pthread key of the image's TLS descriptor and calls `Aps5GuestTlsAllocate` on a thread's first access; the image initializer registers the descriptor with `Aps5GuestTlsRegister`. The thread pointer follows the block and points at itself, as on FreeBSD. `Aps5GuestTlsGetAddr` stands in for `__tls_get_addr`, with the descriptor as the module ID. The key's destructor frees a thread's block. - [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling - [sched_getparam, sched_getscheduler, sched_setparam](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) act only on the calling thread (pid 0), as `pthread_getschedparam` and `scePthreadSetprio` do; another pid fails with `ESRCH`, as for `getpriority`. The own pid throws: FreeBSD 11.4's [`p1003_1b.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/p1003_1b.c#L110-L289) then selects the first thread, which is not tracked. `sched_setparam` throws outside `sched_get_priority_min` to `sched_get_priority_max` (256-767), where FreeBSD's [`ksched_setscheduler`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/ksched.c#L169-L205) answers `EPERM`; `sched_getparam` throws for a thread outside it, such as one `pthread_setschedparam` put in 896-959. The console's answers are unverified. `sched_setscheduler` (needs `PRIV_SCHED_SET`) stays unimplemented - [Linux runtime module initialization](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - modules in explicit runtime directories can be mapped for bindings before an explicit guest load runs their entry and init array once with the caller arguments. Finalizers run only after initialization completed. Console dependency-start and finalization ordering are unmeasured. Windows and macOS use their existing lifecycle paths. From c8cac8e090a081e1fd5e318884b75bbd94e2f4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Tue, 27 Oct 2026 08:42:09 +0100 Subject: [PATCH 400/702] fix(libkernel): initialise the CPU model before reading AVX support for guest TLS Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp b/core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp index a4570c338e..49596b9cb3 100644 --- a/core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp +++ b/core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp @@ -40,7 +40,10 @@ void Release(void* threadPointer) { extern "C" { -std::uint8_t Aps5GuestTlsAvx_nid_no_patch = __builtin_cpu_supports("avx") ? 1 : 0; +std::uint8_t Aps5GuestTlsAvx_nid_no_patch = [] { + __builtin_cpu_init(); + return __builtin_cpu_supports("avx") ? 1 : 0; +}(); void Aps5GuestTlsRegister_nid_no_patch(GuestTlsDescriptor* descriptor) { if (descriptor == nullptr || descriptor->version != 1) Fail("unknown TLS descriptor"); From 244ac01a54652a7d127e71691b0517720735e6a9 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:47:33 -0300 Subject: [PATCH 401/702] test(libc): compile the contraction probe for the macOS build architecture --- core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/tests/LibcFloatingPointContraction.cmake | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index a3b839e5b8..313c616509 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -131,6 +131,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") if(BUILD_TESTING AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND NOT MSVC) add_test(NAME libc_floating_point_contraction COMMAND ${CMAKE_COMMAND} -DCOMPILER=${CMAKE_CXX_COMPILER} + "-DARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" "-DOPTIONS=$<JOIN:$<TARGET_PROPERTY:libc,COMPILE_OPTIONS>, >" -DWORK=${CMAKE_CURRENT_BINARY_DIR}/libc_floating_point_contraction -P ${LIBS_INCLUDE_DIR}/tests/LibcFloatingPointContraction.cmake) diff --git a/core/libs/tests/LibcFloatingPointContraction.cmake b/core/libs/tests/LibcFloatingPointContraction.cmake index 56867679a7..132956d655 100644 --- a/core/libs/tests/LibcFloatingPointContraction.cmake +++ b/core/libs/tests/LibcFloatingPointContraction.cmake @@ -4,8 +4,12 @@ file(MAKE_DIRECTORY "${WORK}") file(WRITE "${source}" "float MultiplyAdd(float a, float b, float c) { return a * b + c; }\n") separate_arguments(options NATIVE_COMMAND "${OPTIONS}") list(FILTER options INCLUDE REGEX "^-f") +set(target) +foreach(architecture IN LISTS ARCHITECTURES) + list(APPEND target -arch ${architecture}) +endforeach() foreach(flags IN ITEMS "" ";${options}") - execute_process(COMMAND "${COMPILER}" -std=gnu++20 -O2 -mfma ${flags} -S "${source}" -o "${assembly}" + execute_process(COMMAND "${COMPILER}" ${target} -std=gnu++20 -O2 -mfma ${flags} -S "${source}" -o "${assembly}" RESULT_VARIABLE result ERROR_VARIABLE error) if(NOT result EQUAL 0) message(FATAL_ERROR "compiling the contraction probe failed: ${error}") From 251107d1eb0bd572063fd16f7c90e83a00b3b5ea Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:58:04 +0200 Subject: [PATCH 402/702] fix(http2): keep teardown consistent under allocation failure --- core/libs/prx/libSceHttp2/CMakeLists.txt | 13 ++ core/libs/tests/Http2TeardownFailure.cpp | 167 +++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 core/libs/tests/Http2TeardownFailure.cpp diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt index 15e7dd4e2f..7c0d0ad3f5 100644 --- a/core/libs/prx/libSceHttp2/CMakeLists.txt +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -56,3 +56,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp2>") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_http2_teardown_tests ${LIBS_INCLUDE_DIR}/tests/Http2TeardownFailure.cpp Export.cpp Cookies.cpp) + target_include_directories(guest_http2_teardown_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http2_teardown_tests PRIVATE libkernel libc anyps5_http2_transport) + configure_windows_unwind(guest_http2_teardown_tests) + add_test(NAME guest_http2_teardown COMMAND guest_http2_teardown_tests) + set_tests_properties(guest_http2_teardown PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_http2_teardown PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() diff --git a/core/libs/tests/Http2TeardownFailure.cpp b/core/libs/tests/Http2TeardownFailure.cpp new file mode 100644 index 0000000000..369e4118e6 --- /dev/null +++ b/core/libs/tests/Http2TeardownFailure.cpp @@ -0,0 +1,167 @@ +#include "prx/libSceHttp2/include/Http2.hpp" +#include <array> +#include <atomic> +#include <cstddef> +#include <cstdio> +#include <cstdlib> +#include <exception> +#include <new> +#include <thread> + +static thread_local int allocationBudget = -1; +static thread_local unsigned allocationAttempts = 0; + +void* operator new(std::size_t bytes) { + if (allocationBudget >= 0) { + ++allocationAttempts; + if (allocationBudget == 0) throw std::bad_alloc(); + --allocationBudget; + } + if (void* memory = std::malloc(bytes ? bytes : 1)) return memory; + throw std::bad_alloc(); +} + +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { ::operator delete(memory); } + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +constexpr int InvalidId = static_cast<int>(0x817B1100); + +enum class Operation { None, Request, Template, Context }; + +static const char* Name(Operation operation) { + switch (operation) { + case Operation::None: break; + case Operation::Request: return "DeleteRequest"; + case Operation::Template: return "DeleteTemplate"; + case Operation::Context: return "Term"; + } + std::abort(); +} + +struct Tree { + int context; + std::array<int, 2> templates; + std::array<int, 6> requests; + + Tree() : context(sceHttp2Init(1, 1, 0x10000, 4)) { + Require(context > 0, "context creation failed"); + for (auto& id : templates) { + id = sceHttp2CreateTemplate(context, "agent", 3, 0); + Require(id > 0, "template creation failed"); + } + for (std::size_t index = 0; index < requests.size(); ++index) { + requests[index] = sceHttp2CreateRequestWithURL(templates[index / 3], "GET", "https://example.invalid/", 0); + Require(requests[index] > 0, "request creation failed"); + } + } + + int Delete(Operation operation) const { + switch (operation) { + case Operation::None: break; + case Operation::Request: return sceHttp2DeleteRequest(requests[0]); + case Operation::Template: return sceHttp2DeleteTemplate(templates[0]); + case Operation::Context: return sceHttp2Term(context); + } + std::abort(); + } +}; + +static bool ContextExists(int context) { + const int probe = sceHttp2CreateTemplate(context, "probe", 3, 0); + if (probe == InvalidId) return false; + Require(probe > 0, "context probe failed"); + Require(sceHttp2DeleteTemplate(probe) == 0, "context probe cleanup failed"); + return true; +} + +static bool CheckTree(const Tree& tree, Operation operation) { + bool correct = true; + for (std::size_t index = 0; index < tree.requests.size(); ++index) { + const bool removed = operation == Operation::Context || (operation == Operation::Template ? index < 3 : operation == Operation::Request && index == 0); + const int status = sceHttp2SslEnableOption(tree.requests[index], 0); + if (status != (removed ? InvalidId : 0)) correct = false; + } + if (ContextExists(tree.context) != (operation != Operation::Context)) correct = false; + for (std::size_t index = 0; index < tree.templates.size(); ++index) { + const bool removed = operation == Operation::Context || (operation == Operation::Template && index == 0); + if (sceHttp2SslEnableOption(tree.templates[index], 0) != (removed ? InvalidId : 0)) correct = false; + } + return correct; +} + +static unsigned AllocationFailureCases(Operation operation) { + unsigned failures = 0; + for (int budget = 0; budget < 16; ++budget) { + Tree tree; + Tree independent; + const int cookieBox = sceHttp2CreateCookieBox(0x10000); + Require(cookieBox > 0, "cookie box creation failed"); + allocationAttempts = 0; + allocationBudget = budget; + bool threw = false; + int result = -1; + try { result = tree.Delete(operation); } + catch (const std::bad_alloc&) { threw = true; } + allocationBudget = -1; + const bool correct = CheckTree(tree, operation); + if (threw || result != 0 || !correct || allocationAttempts != 0) { + std::fprintf(stderr, "%s budget=%d threw=%d result=%d ownership=%d allocation attempts=%u\n", + Name(operation), budget, threw, result, correct, allocationAttempts); + ++failures; + } + Require(CheckTree(independent, Operation::None), "independent context changed"); + Http2CookieStats stats{}; + Require(sceHttp2GetCookieStats(cookieBox, &stats) == 0, "unrelated cookie box was deleted"); + Require(sceHttp2DeleteCookieBox(cookieBox) == 0, "cookie box cleanup failed"); + Require(sceHttp2Term(independent.context) == 0, "independent context cleanup failed"); + if (ContextExists(tree.context)) + Require(sceHttp2Term(tree.context) == 0, "context cleanup failed"); + } + return failures; +} + +static void ConcurrentCreationCases(Operation operation) { + for (int iteration = 0; iteration < 64; ++iteration) { + Tree tree; + std::array<int, 16> created{}; + std::atomic<bool> start{false}; + std::thread producer([&] { + while (!start.load(std::memory_order_acquire)) std::this_thread::yield(); + for (auto& request : created) { + request = sceHttp2CreateRequestWithURL(tree.templates[0], "GET", "https://example.invalid/", 0); + Require(request > 0 || request == InvalidId, "concurrent creation returned an unexpected result"); + } + }); + start.store(true, std::memory_order_release); + Require(tree.Delete(operation) == 0, "concurrent deletion failed"); + producer.join(); + Require(CheckTree(tree, operation), "concurrent deletion changed object ownership"); + for (const int request : created) { + if (request == InvalidId) { + Require(operation != Operation::Request, "request deletion removed its parent template"); + continue; + } + const int expected = operation == Operation::Request ? 0 : InvalidId; + Require(sceHttp2SslEnableOption(request, 0) == expected, "concurrent creation escaped its parent's teardown"); + } + Require(tree.Delete(operation) == InvalidId, "repeated deletion succeeded"); + if (operation != Operation::Context) Require(sceHttp2Term(tree.context) == 0, "concurrent fixture cleanup failed"); + } +} + +int main() { + unsigned failures = 0; + for (const auto operation : {Operation::Request, Operation::Template, Operation::Context}) { + failures += AllocationFailureCases(operation); + ConcurrentCreationCases(operation); + } + std::printf("48 allocation-pressure lifecycles, 192 concurrent lifecycles, %u failures\n", failures); + return failures == 0 ? 0 : 1; +} From 422ce09841363c945d986b1cfa48aca7f5c02c6c Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:49:24 +0200 Subject: [PATCH 403/702] test(http2): disarm allocation fault before throwing --- core/libs/tests/Http2TeardownFailure.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/Http2TeardownFailure.cpp b/core/libs/tests/Http2TeardownFailure.cpp index 369e4118e6..00b2ecc827 100644 --- a/core/libs/tests/Http2TeardownFailure.cpp +++ b/core/libs/tests/Http2TeardownFailure.cpp @@ -14,7 +14,10 @@ static thread_local unsigned allocationAttempts = 0; void* operator new(std::size_t bytes) { if (allocationBudget >= 0) { ++allocationAttempts; - if (allocationBudget == 0) throw std::bad_alloc(); + if (allocationBudget == 0) { + allocationBudget = -1; + throw std::bad_alloc(); + } --allocationBudget; } if (void* memory = std::malloc(bytes ? bytes : 1)) return memory; From baa492dcc9a65dc7ef1e5e69debdbaa61f30f6e2 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:49:55 +0800 Subject: [PATCH 404/702] fix(tools): preserve configure seeds when writing fails --- tools/configure_seed.py | 28 +++++++++++-- tools/tests/test_configure_seed.py | 63 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index ba93ee709d..4f3d58a053 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -1,6 +1,9 @@ import argparse import fnmatch +import os import re +import secrets +import stat import sys from pathlib import Path @@ -31,6 +34,26 @@ def quote(value): return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$") + '"' +def write_seed(path, text): + destination = path.resolve() + metadata = destination.stat() if destination.exists() else None + if metadata is not None and os.name == "nt" and metadata.st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: + raise PermissionError(f"Seed output is read-only: {path}") + destination.parent.mkdir(parents=True, exist_ok=True) + temporary = None + try: + candidate = destination.parent / (".configure-seed-" + secrets.token_hex(16)) + with candidate.open("x", encoding="utf-8", newline="\n") as output: + temporary = candidate + if metadata is not None: + temporary.chmod(stat.S_IMODE(metadata.st_mode)) + output.write(text) + os.replace(temporary, destination) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): @@ -54,9 +77,8 @@ def main(argv=None): if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} try: - seed.parent.mkdir(parents=True, exist_ok=True) - seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" - for name, (value, doc) in sorted(found.items())), encoding="utf-8") + write_seed(seed, "".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items()))) except OSError as error: parser.error(f"{seed}: {error}") print(f"{len(found)} check results") diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 373ce43764..d528af49c3 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -37,6 +37,69 @@ set(MINI_OPTION "value" CACHE STRING "An option, not a check result") class ConfigureSeedPathTests(unittest.TestCase): + @unittest.skipUnless(os.name == "posix", "requires POSIX file size limits") + def test_failed_write_preserves_existing_seed_and_allows_retry(self): + seed = self.root / "seed.cmake" + original = b"previous complete seed\n" + seed.write_bytes(original) + self.cache.write_text("//Test LONG_FEATURE\nLONG_FEATURE:INTERNAL=" + "x" * 300 + "\n", encoding="utf-8") + program = ("import resource, signal, sys; signal.signal(signal.SIGXFSZ, signal.SIG_IGN); " + "resource.setrlimit(resource.RLIMIT_FSIZE, (64, 64)); " + "sys.path.insert(0, sys.argv[1]); import configure_seed; configure_seed.main(sys.argv[2:])") + result = subprocess.run([sys.executable, "-c", program, str(TOOL.parent), str(self.cache), str(seed)], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + self.assertEqual(seed.read_bytes(), original) + self.assertEqual(list(self.root.glob(".configure-seed-*")), []) + result = self.run_seed(seed) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("LONG_FEATURE", seed.read_text(encoding="utf-8")) + + @unittest.skipUnless(os.name == "posix", "requires POSIX file modes") + def test_existing_modes_and_new_file_umask_are_preserved(self): + seed = self.root / "seed.cmake" + program = "import os,sys; os.umask(0o027); sys.path.insert(0,sys.argv[1]); import configure_seed; configure_seed.main(sys.argv[2:])" + arguments = [sys.executable, "-c", program, str(TOOL.parent), str(self.cache), str(seed)] + result = subprocess.run(arguments, capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(seed.stat().st_mode & 0o777, 0o640) + seed.chmod(0o600) + result = subprocess.run(arguments, capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(seed.stat().st_mode & 0o777, 0o600) + + @unittest.skipUnless(os.name == "nt", "requires Windows read-only attributes") + def test_read_only_seed_is_not_replaced_and_leaves_no_temporary_file(self): + seed = self.root / "seed.cmake" + original = b"old seed\n" + seed.write_bytes(original) + seed.chmod(0o444) + try: + result = self.run_seed(seed) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(seed.read_bytes(), original) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(list(self.root.glob(".configure-seed-*")), []) + finally: + seed.chmod(0o666) + + def test_output_symlink_keeps_the_link_and_updates_its_target(self): + target = self.root / "target.cmake" + target.write_text("old seed", encoding="utf-8") + seed = self.root / "seed.cmake" + try: + seed.symlink_to(target) + except OSError as error: + if os.name == "nt" and getattr(error, "winerror", None) == 1314: + self.skipTest("symbolic link creation needs Windows privilege") + raise + result = self.run_seed(seed) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue(seed.is_symlink()) + self.assertIn("HAVE_FEATURE", target.read_text(encoding="utf-8")) + def setUp(self): self.directory = tempfile.TemporaryDirectory() self.addCleanup(self.directory.cleanup) From b5ab136f4b30500916c948fcad44d285c7d41c98 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:49:56 +0800 Subject: [PATCH 405/702] fix(web): validate every memory estimate input --- demo/web/batch.mjs | 1 + demo/web/tests/batch.test.mjs | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/demo/web/batch.mjs b/demo/web/batch.mjs index 5058dbea4f..1899532f98 100644 --- a/demo/web/batch.mjs +++ b/demo/web/batch.mjs @@ -6,6 +6,7 @@ function safeSize(value) { } export function memoryEstimate(archiveBytes, selectedBytes, libraryBytes, entries) { + for (const value of [archiveBytes, selectedBytes, libraryBytes, entries]) safeSize(value); return safeSize(Math.ceil(archiveBytes * 1.02) + selectedBytes * 4 + libraryBytes + entries * 1024 + 8 * 1024 * 1024); } diff --git a/demo/web/tests/batch.test.mjs b/demo/web/tests/batch.test.mjs index b19827d72b..fb774ac7d1 100644 --- a/demo/web/tests/batch.test.mjs +++ b/demo/web/tests/batch.test.mjs @@ -2,6 +2,20 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { batchMemoryEstimate, batchOutputDirectory, memoryEstimate, MEMORY_LIMIT } from "../batch.mjs"; +test("rejects each invalid memory operand before arithmetic can coerce or cancel it", () => { + for (let index = 0; index < 4; index++) { + for (const value of [-1, -100000000, "1024", null, true, NaN, Infinity, 1.5, Number.MAX_SAFE_INTEGER + 1]) { + const operands = [1024, 70 * 1024 * 1024, 0, 2]; + operands[index] = value; + assert.throws(() => memoryEstimate(...operands), /unsupported file size/u); + } + } + const summaries = [{totalBytes: 1024, selectedBytes: 70 * 1024 * 1024, windowsSelectedBytes: 70 * 1024 * 1024, files: 2}]; + assert.ok(batchMemoryEstimate(summaries, "linux", 0) > MEMORY_LIMIT); + assert.throws(() => batchMemoryEstimate(summaries, "linux", -100000000), /unsupported file size/u); + assert.equal(memoryEstimate(0, 0, 0, 0), 8 * 1024 * 1024); +}); + test("estimates all ZIP outputs against one shared memory limit", () => { const summaries = [ { totalBytes: 1024, selectedBytes: 128, windowsSelectedBytes: 256, files: 2 }, From b090ce80046a0f72229902d6c230f2e056c56d75 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:49:59 +0800 Subject: [PATCH 406/702] fix(tools): recognize trailing return export definitions --- tools/nid_names.py | 2 +- tools/tests/test_nid_names.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index 920876f1b3..7191fff473 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -24,7 +24,7 @@ CPP_TOKEN = re.compile( r'|(?P<string>(?:u8|u|U|L)?"(?:\\[\s\S]|[^"\\])*")' r"|(?P<char>(?:u8|u|U|L)?'(?:\\[\s\S]|[^'\\])*')" r"|(?P<code>(?:[0-9]|\.[0-9])[A-Za-z0-9_.']*|[A-Za-z_][A-Za-z0-9_]*|\S)") -DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") +DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:->\s*[^;{]+)?(?:try\s*)?\{") NID_POSTFIX = "_nid_postfix" ASM_FUNCTION_MACROS = {"APS5_ASM_FUNCTION", "APS5_SIGNAL_ASM_FUNCTION"} diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 59e76c64ca..7d1b0b4a6d 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -398,6 +398,27 @@ class NidArgumentTests(unittest.TestCase): class SourceCollectionTests(unittest.TestCase): + def test_trailing_return_definitions_are_collected_without_declarations_or_literals(self): + text = ('auto APS5_VABI sceTrailing() -> int { return 0; }\n' + 'auto APS5_VABI sceNoThrow() noexcept(true) -> unsigned long { return 0; }\n' + 'auto APS5_VABI pointer_nid_postfix() -> const char* { return nullptr; }\n' + 'auto APS5_VABI sceDeclaration() -> int;\n' + 'const char* example = "auto APS5_VABI sceQuoted() -> int { return 0; }";\n' + '/* auto APS5_VABI sceComment() -> int { return 0; } */\n') + self.write_source(text) + expected = {"sceTrailing", "sceNoThrow", "pointer", "pointer_nid_postfix"} + self.assertEqual(nid_names.collect_real_names(), expected) + matches = list(nid_names.source_definitions(text)) + self.assertEqual([match.group(1) for match in matches], ["sceTrailing", "sceNoThrow", "pointer_nid_postfix"]) + self.assertTrue(all(text[match.end() - 1] == "{" for match in matches)) + + def test_trailing_return_export_names_reach_the_overlap_consumer(self): + import pr_overlap + + text = "auto APS5_VABI sceTrailing() noexcept(false) -> int { return 0; }\n" + exports = pr_overlap.source_exports(text, "core/libs/prx/libSceExample") + self.assertEqual(exports, {("core/libs/prx/libSceExample", nid_names.compute_nid("sceTrailing")): {"sceTrailing"}}) + def setUp(self): self.directory = tempfile.TemporaryDirectory() self.addCleanup(self.directory.cleanup) From 54a8d7c411cdfc7ff26ba57a382f34a507ae94c0 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:50:01 +0800 Subject: [PATCH 407/702] fix(tools): report the input mapper listening port --- tools/input-mapper/serve.js | 2 +- tools/input-mapper/tests/serve.test.js | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/tools/input-mapper/serve.js b/tools/input-mapper/serve.js index 7026d55016..05d84b65c3 100644 --- a/tools/input-mapper/serve.js +++ b/tools/input-mapper/serve.js @@ -34,4 +34,4 @@ server.on('error', error => { console.error(error.message); process.exitCode = 1; }); -server.listen(port, '127.0.0.1', () => console.log(`Input mapper: http://127.0.0.1:${port}`)); +server.listen(port, '127.0.0.1', () => console.log(`Input mapper: http://127.0.0.1:${server.address().port}`)); diff --git a/tools/input-mapper/tests/serve.test.js b/tools/input-mapper/tests/serve.test.js index 72a5461928..f0e9cd2f0f 100644 --- a/tools/input-mapper/tests/serve.test.js +++ b/tools/input-mapper/tests/serve.test.js @@ -6,14 +6,14 @@ import net from 'node:net'; import {fileURLToPath} from 'node:url'; import {setTimeout as delay} from 'node:timers/promises'; -async function startServer(t) { +async function startServer(t, ephemeral = false) { const reservation = net.createServer(); reservation.listen(0, '127.0.0.1'); await once(reservation, 'listening'); const port = reservation.address().port; await new Promise((resolve, reject) => reservation.close(error => error ? reject(error) : resolve())); const child = spawn(process.execPath, [fileURLToPath(new URL('../serve.js', import.meta.url))], { - env: {...process.env, PORT: String(port)}, stdio: ['ignore', 'pipe', 'pipe'] + env: {...process.env, PORT: ephemeral ? '0' : String(port)}, stdio: ['ignore', 'pipe', 'pipe'] }); let stderr = ''; child.stderr.setEncoding('utf8').on('data', text => { stderr += text; }); @@ -27,13 +27,28 @@ async function startServer(t) { let ready = ''; child.stdout.setEncoding('utf8').on('data', text => { ready += text; }); for (let attempts = 0; attempts < 500; attempts++) { - if (ready.includes('Input mapper:')) return {child, port, stderr: () => stderr}; + if (ready.includes('Input mapper:')) { + const match = /Input mapper: http:\/\/127\.0\.0\.1:(\d+)/u.exec(ready); + assert.ok(match, ready); + const actual = Number(match[1]); + assert.ok(actual > 0 && actual <= 65535, ready); + if (!ephemeral) assert.equal(actual, port); + return {child, port: actual, stderr: () => stderr}; + } if (child.exitCode !== null) throw new Error(stderr || `server exited ${child.exitCode}`); await delay(10); } throw new Error('server did not become ready'); } +test('a dynamically assigned port is reported as a usable server address', {timeout: 15000}, async t => { + const server = await startServer(t, true); + assert.match(await rawRequest(server.port, '/'), /^HTTP\/1\.1 200 /u); + assert.match(await rawRequest(server.port, '/mapping.js', 'HEAD'), /^HTTP\/1\.1 200 /u); + assert.equal(server.child.exitCode, null); + assert.equal(server.stderr(), ''); +}); + async function rawRequest(port, target, method = 'GET') { return new Promise((resolve, reject) => { const socket = net.createConnection({host: '127.0.0.1', port}); From d6ec92312c0367a34ac159f2117494b438576892 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:14:19 +0100 Subject: [PATCH 408/702] test(libkernel): cover pthread_setspecific and pthread_getspecific pthread_setspecific returns an errno value, unlike the Sce function it wraps, and neither POSIX export had a test. Check the FreeBSD 11.4 results of _pthread_setspecific and _pthread_getspecific (lib/libthr/thread/thr_spec.c lines 165-194 and 196-222): a key outside the valid range or a deleted key makes setspecific return EINVAL (22), getspecific returns NULL for such a key and for a key never set, a stored value is read back, NULL can be stored, a new key with a reused number starts empty, and another thread has its own value. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ core/libs/tests/GuestPosixSpecific.cpp | 77 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 core/libs/tests/GuestPosixSpecific.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 974df3b88c..d0367998e4 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1998,6 +1998,16 @@ if(BUILD_TESTING) set_tests_properties(guest_fstat_fault PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") endif() + add_test_executable(guest_posix_specific_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSpecific.cpp) + target_include_directories(guest_posix_specific_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_specific_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_posix_specific_tests) + add_test(NAME guest_posix_specific COMMAND guest_posix_specific_tests) + set_tests_properties(guest_posix_specific PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_posix_specific PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") + endif() add_test_executable(guest_rename_dot_tests ${LIBS_INCLUDE_DIR}/tests/GuestRenameDot.cpp) target_include_directories(guest_rename_dot_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_rename_dot_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestPosixSpecific.cpp b/core/libs/tests/GuestPosixSpecific.cpp new file mode 100644 index 0000000000..aaa308675a --- /dev/null +++ b/core/libs/tests/GuestPosixSpecific.cpp @@ -0,0 +1,77 @@ +#include "prx/libkernel/include/Api.hpp" +#include "SceTypes.hpp" + +#include <atomic> +#include <chrono> +#include <cstdio> +#include <cstdlib> +#include <thread> + +namespace { + +constexpr int Einval = 22; + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +void WaitFor(const std::atomic<bool>& flag, const char* message) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!flag && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(flag, message); +} + +} + +int main() { + int first = 0; + int second = 0; + for (const PthreadKey invalid : {static_cast<PthreadKey>(0), static_cast<PthreadKey>(0xFFFFFFFFu)}) { + Require(pthread_setspecific_nid_postfix(invalid, &first) == Einval, "setspecific of an out-of-range key returns EINVAL"); + Require(pthread_getspecific_nid_postfix(invalid) == nullptr, "getspecific of an out-of-range key returns NULL"); + } + + PthreadKey key = 0; + Require(pthread_key_create_nid_postfix(&key, nullptr) == 0 && key != 0, "key create"); + Require(pthread_getspecific_nid_postfix(key) == nullptr, "getspecific of a key never set returns NULL"); + Require(pthread_setspecific_nid_postfix(key, &first) == 0, "setspecific returns 0"); + Require(pthread_getspecific_nid_postfix(key) == &first, "getspecific returns the stored value"); + Require(pthread_setspecific_nid_postfix(key, nullptr) == 0, "setspecific of NULL returns 0"); + Require(pthread_getspecific_nid_postfix(key) == nullptr, "getspecific after storing NULL returns NULL"); + Require(pthread_setspecific_nid_postfix(key, &first) == 0, "setspecific again returns 0"); + + std::atomic<bool> stored{false}; + std::atomic<bool> release{false}; + std::atomic<bool> finished{false}; + std::atomic<void*> seenBefore{&first}; + std::atomic<void*> seenAfter{nullptr}; + std::thread other([&] { + seenBefore = pthread_getspecific_nid_postfix(key); + Require(pthread_setspecific_nid_postfix(key, &second) == 0, "helper setspecific returns 0"); + seenAfter = pthread_getspecific_nid_postfix(key); + stored = true; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!release && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + finished = true; + }); + WaitFor(stored, "helper never stored its value"); + Require(seenBefore.load() == nullptr, "another thread starts with NULL for a key this thread has set"); + Require(seenAfter.load() == &second, "another thread reads back its own value"); + Require(pthread_getspecific_nid_postfix(key) == &first, "this thread still reads its own value"); + release = true; + WaitFor(finished, "helper never finished"); + other.join(); + + Require(pthread_key_delete_nid_postfix(key) == 0, "key delete returns 0"); + Require(pthread_getspecific_nid_postfix(key) == nullptr, "getspecific of a deleted key returns NULL"); + Require(pthread_setspecific_nid_postfix(key, &first) == Einval, "setspecific of a deleted key returns EINVAL"); + + PthreadKey reused = 0; + Require(pthread_key_create_nid_postfix(&reused, nullptr) == 0 && reused == key, "a new key reuses the freed number"); + Require(pthread_getspecific_nid_postfix(reused) == nullptr, "the value stored under the old key is gone"); + Require(pthread_key_delete_nid_postfix(reused) == 0, "second key delete returns 0"); + return 0; +} From 4b102f0603892875224b9b123fff20b057bff9b6 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:49:57 +0800 Subject: [PATCH 409/702] fix(tools): enforce validation gate test timeout limits --- tools/tests/test_vulkan_validation_gate.py | 43 ++++++++++++++++++++++ tools/vulkan_validation_gate.py | 14 +++++++ 2 files changed, 57 insertions(+) diff --git a/tools/tests/test_vulkan_validation_gate.py b/tools/tests/test_vulkan_validation_gate.py index fc31ac1983..b2fae98737 100644 --- a/tools/tests/test_vulkan_validation_gate.py +++ b/tools/tests/test_vulkan_validation_gate.py @@ -16,6 +16,16 @@ import vulkan_validation_gate as gate class ReportTests(unittest.TestCase): + def test_effective_timeouts_cannot_override_the_requested_limit(self): + for value in (0, 2, -1, float("inf"), float("nan"), "1", True): + with self.subTest(value=value), self.assertRaisesRegex(gate.GateError, "probe: TIMEOUT"): + gate.check_timeouts([{"name": "probe", "properties": [{"name": "TIMEOUT", "value": value}]}], 1) + for value in (.05, 1): + gate.check_timeouts([{"name": "probe", "properties": [{"name": "TIMEOUT", "value": value}]}], 1) + gate.check_timeouts([{"name": "default", "properties": []}], 1) + with self.assertRaisesRegex(gate.GateError, "TIMEOUT_AFTER_MATCH"): + gate.check_timeouts([{"name": "probe", "properties": [{"name": "TIMEOUT_AFTER_MATCH", "value": ["1", "ready"]}]}], 1) + def report(self, text): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "junit.xml" @@ -142,6 +152,39 @@ class FixtureTests(unittest.TestCase): class IntegrationTests(FixtureTests): + def test_timeout_reset_property_is_refused_before_running_tests(self): + baseline = self.source("base-reset") + candidate = self.source("head-reset", property_text='TIMEOUT_AFTER_MATCH "1;ready"') + code, report = self.execute(baseline, candidate, "--repeat", "1", "--test-timeout", "1") + self.assertEqual(code, 2, report) + self.assertIn("TIMEOUT_AFTER_MATCH", report["error"]) + self.assertEqual(report["iterations"], []) + + def test_a_shorter_ctest_property_remains_usable(self): + baseline = self.source("base-short", property_text="TIMEOUT 0.5") + candidate = self.source("head-short", property_text="TIMEOUT 0.5") + code, report = self.execute(baseline, candidate, "--repeat", "1", "--test-timeout", "1") + self.assertEqual(code, 0, report) + self.assertEqual(report["status"], "complete") + + def test_request_above_thirty_seconds_is_refused_before_output_creation(self): + with self.assertRaises(SystemExit) as raised: + self.execute(self.source("base-limit"), self.source("head-limit"), "--test-timeout", "31") + self.assertEqual(raised.exception.code, 2) + self.assertFalse((self.root / "evidence").exists()) + + def test_ctest_timeout_property_is_checked_before_running_tests(self): + for value in ("0", "2"): + with self.subTest(value=value): + baseline = self.source("base-" + value) + candidate = self.source("head-" + value, property_text="TIMEOUT " + value) + code, report = self.execute(baseline, candidate, "--repeat", "1", "--test-timeout", ".05") + self.assertEqual(code, 2, report) + self.assertIn("probe: TIMEOUT", report["error"]) + self.assertEqual(report["iterations"], []) + self.assertEqual(report["status"], "incomplete") + (self.root / "evidence").rename(self.root / ("evidence-" + value)) + def test_pass_builds_both_targets_and_alternates_with_attribution(self): code, report = self.execute(self.source("base"), self.source("head")) self.assertEqual(code, 0, report) diff --git a/tools/vulkan_validation_gate.py b/tools/vulkan_validation_gate.py index d4fa754a81..4bea30cbba 100644 --- a/tools/vulkan_validation_gate.py +++ b/tools/vulkan_validation_gate.py @@ -99,6 +99,17 @@ def artifact_hashes(build, tests, runtime): return {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted(paths)} +def check_timeouts(tests, limit): + for test in tests: + for prop in test.get("properties", []): + if prop["name"] == "TIMEOUT_AFTER_MATCH": + raise GateError(f"{test['name']}: TIMEOUT_AFTER_MATCH can restart the requested test timeout") + if prop["name"] == "TIMEOUT": + value = prop["value"] + if type(value) not in (int, float) or not math.isfinite(value) or not 0 < value <= limit: + raise GateError(f"{test['name']}: TIMEOUT must be positive and at most {limit} seconds") + + def parse_report(path, expected): root = ET.parse(path).getroot() if root.tag not in ("testsuite", "testsuites"): @@ -168,6 +179,8 @@ def main(argv=None): args = parser.parse_args(argv) if args.repeat <= 0: parser.error("--repeat must be positive") + if args.test_timeout > 30: + parser.error("--test-timeout must be at most 30 seconds") if any(not re.fullmatch(r"-D[A-Za-z_][A-Za-z0-9_]*(?::[A-Z]+)?=.*", value) for value in args.cmake_arg): parser.error("--cmake-arg accepts only -D cache definitions") report = {"status": "incomplete", "iterations": [], "builds": {}, @@ -221,6 +234,7 @@ def main(argv=None): names = [test["name"] for test in tests] if not names or len(names) != len(set(names)): raise GateError(f"{side}: expected nonempty unique test names") + check_timeouts(tests, args.test_timeout) for test in tests: for prop in test.get("properties", []): if prop["name"] in ("ENVIRONMENT", "ENVIRONMENT_MODIFICATION"): From 5bb4711bb88caa03d505f12f0ad8856c84020215 Mon Sep 17 00:00:00 2001 From: slopgobblercobbler <slopgobblercobbler@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:18:02 +0100 Subject: [PATCH 410/702] fix(libSceSaveData.native): return SAVE_DATA_ERROR_PARAMETER on null argument in sceSaveDataConvert --- core/libs/prx/libSceSaveData.native/Export.cpp | 2 +- core/libs/tests/GuestSaveDataConvert.cpp | 15 +++------------ docs/dev/debt/libSceSaveData.native.md | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index e32d58bb14..b5f3008eb8 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -940,7 +940,7 @@ int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, static int convert(const SaveDataConvert* param) { if (param == nullptr || param->dir_name == nullptr) { - throw std::runtime_error("sceSaveDataConvert: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } g_convertProgress = 0.0f; const auto* nameEnd = static_cast<const char*>(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp index 13085f00db..5833f5029d 100644 --- a/core/libs/tests/GuestSaveDataConvert.cpp +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -27,15 +27,6 @@ void Check(bool condition, const std::string& what) { ++failures; } -bool Throws(const SaveDataConvert* convert) { - try { - sceSaveDataConvert(convert); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - int Convert(const char* data, std::size_t size) { SceSaveDataDirName name{}; std::memcpy(name.data, data, size); @@ -63,13 +54,13 @@ int main() { float progress = 0.0f; Check(sceSaveDataGetConvertProgress(&progress) == SaveDataErrorNotInitialized, "query before initialize fails with not initialized"); - Check(Throws(nullptr), "null convert pointer throws"); - SaveDataConvert nullDir{}; - Check(Throws(&nullDir), "null dir_name throws"); Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds"); Check(progress == 0.0f, "convert progress reports 0.0f initially"); Check(sceSaveDataGetConvertProgress(nullptr) == SaveDataErrorParameter, "null progress pointer is rejected"); + Check(sceSaveDataConvert(nullptr) == SaveDataErrorParameter, "null convert pointer is rejected"); + SaveDataConvert nullDir{}; + Check(sceSaveDataConvert(&nullDir) == SaveDataErrorParameter, "null dir_name pointer is rejected"); Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds after failed convert"); Check(progress < 1.0f, "convert progress is below 1.0f after failed convert"); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 3249bb0f9a..a3817535a9 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -19,6 +19,6 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. One title (PPSA35669, in a plugin) disassembled for this entry has one call site (0x2eda71); it clears 56 bytes for the result structure first (the size of the structure used here), puts a value returned by a call with the argument 0x8000 at offset 8 and 0x400 at offset 0x10, and afterwards branches on a negative result and reads the 32-bit field at offset 0, skipping its loop when it is 0. That agrees with the result structure of `sceSaveDataDirNameSearch`. The condition structure was not read. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2`. In PPSA05347 the function that calls it at 0x235fbb5 zeroes its 0x40-byte result before the call and, for a negative result other than 0x809f0008 and 0x809f000f (BUSY included), logs the code and returns it without recording the mount; no abort follows. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware -- [sceSaveDataBackup, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataDirNameSearchPs4, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataBackup, sceSaveDataConvert, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataDirNameSearchPs4, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob From 7a0dfeb79c3d440738cf87762eafe07c96ceb1cf Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:58:57 +0100 Subject: [PATCH 411/702] test(http2): link teardown fixture with Windows sockets --- core/libs/prx/libSceHttp2/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt index 7c0d0ad3f5..4379edd8d0 100644 --- a/core/libs/prx/libSceHttp2/CMakeLists.txt +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -65,6 +65,7 @@ if(BUILD_TESTING) add_test(NAME guest_http2_teardown COMMAND guest_http2_teardown_tests) set_tests_properties(guest_http2_teardown PROPERTIES TIMEOUT 30) if(WIN32) + target_link_libraries(guest_http2_teardown_tests PRIVATE ws2_32) set_tests_properties(guest_http2_teardown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() From daa5cf02e81ba8820822c0f929e133213809d2d0 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 08:01:44 +0000 Subject: [PATCH 412/702] test(agc): check the action sceAgcQueueEndOfPipeActionPatchType writes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgc/CMakeLists.txt | 13 ++++++ .../libSceAgc/tests/EndOfPipeActionType.cpp | 43 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 core/libs/prx/libSceAgc/tests/EndOfPipeActionType.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 82331aea05..4fc6a1edf4 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -227,6 +227,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_end_of_pipe_type_patch_tests EXCLUDE_FROM_ALL tests/EndOfPipeActionType.cpp) +target_include_directories(agc_end_of_pipe_type_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_end_of_pipe_type_patch_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_end_of_pipe_type_patch_tests) +if(BUILD_TESTING) + set_target_properties(agc_end_of_pipe_type_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_end_of_pipe_type_patch COMMAND agc_end_of_pipe_type_patch_tests) + if(WIN32) + set_tests_properties(agc_end_of_pipe_type_patch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() + add_test_executable(agc_branch_patch_tests EXCLUDE_FROM_ALL tests/BranchPatch.cpp Command/src/Packet.cpp diff --git a/core/libs/prx/libSceAgc/tests/EndOfPipeActionType.cpp b/core/libs/prx/libSceAgc/tests/EndOfPipeActionType.cpp new file mode 100644 index 0000000000..dd2ecba3fd --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/EndOfPipeActionType.cpp @@ -0,0 +1,43 @@ +#include "prx/libSceAgc/Cb/include/Sync.hpp" +#include "prx/libSceAgc/Patch/include/QueueEndOfPipeAction.hpp" +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstdio> + +namespace { + +struct Storage { + std::array<std::uint32_t, 16> words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; +}; + +std::array<std::uint32_t, 8> release(std::uint8_t action, const volatile Label* label) { + Storage storage; + const auto* packet = sceAgcCbReleaseMem(&storage.buffer, action, 0x0a5, 1, 2, label, 2, 0x1122334455667788ull, 0, 0, 0, 0); + std::array<std::uint32_t, 8> words{}; + for (std::size_t index = 0; index < words.size(); ++index) words[index] = packet[index]; + return words; +} + +int fail(const char* message) { + std::fprintf(stderr, "%s\n", message); + return 1; +} + +} + +int main() { + alignas(8) static Label label{}; + auto packet = release(0x28, &label); + const auto original = packet; + for (const std::uint8_t action : {0x00u, 0x14u, 0x2eu, 0x2fu, 0x30u, 0x3fu}) { + if (sceAgcQueueEndOfPipeActionPatchType(packet.data(), action) != 0) return fail("the end-of-pipe type patch failed"); + if (packet != release(action, &label)) return fail("a patched end-of-pipe action differs from a release built with that action"); + } + if (sceAgcQueueEndOfPipeActionPatchType(packet.data(), 0x28) != 0 || packet != original) return fail("patching the action back did not restore the packet"); + std::puts("AGC end-of-pipe type patch tests passed"); + return 0; +} From a847058156bf92dc1c8ab07b164d735021768963 Mon Sep 17 00:00:00 2001 From: Hedi Trigui <hedi.trigui.13@gmail.com> Date: Mon, 26 Oct 2026 20:13:28 +0100 Subject: [PATCH 413/702] test(libSceNpCommerce): cover uninitialized and terminated argument validation --- core/libs/tests/GuestNpCommerceDialog.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp index 58383cb878..28a329dd02 100644 --- a/core/libs/tests/GuestNpCommerceDialog.cpp +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -195,7 +195,15 @@ int main() { Require(sceNpCommerceDialogUpdateStatus() == statusFinished); RequireCanceledResult(param2.userData, false); Require(sceNpCommerceDialogTerminate() == 0); + Require(sceNpCommerceDialogTerminate() == static_cast<int>(0x80B80003u)); + Require(sceNpCommerceDialogClose() == static_cast<int>(0x80B80003u)); + Require(sceNpCommerceDialogOpen(nullptr) == static_cast<int>(0x80B8000Du)); + Require(sceNpCommerceDialogOpen2(nullptr) == static_cast<int>(0x80B8000Du)); + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast<int>(0x80B80003u)); + Require(sceNpCommerceDialogGetResult(nullptr) == static_cast<int>(0x80B8000Du)); + Require(sceNpCommerceDialogGetResult(&result) == static_cast<int>(0x80B80003u)); Require(sceNpCommerceDialogGetStatus() == statusNone); + Require(sceNpCommerceDialogUpdateStatus() == statusNone); Require(sceUserServiceTerminate() == 0); return 0; } From 9618254977e4dcf201d1e2660038c9cc796a8ff8 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:16:22 +0100 Subject: [PATCH 414/702] fix(libc): reopen a closed stream in freopen instead of aborting --- core/libs/prx/libc/CMakeLists.txt | 3 +- core/libs/prx/libc/include/FileStream.hpp | 33 +++++++++++++----- core/libs/prx/libc/src/FileAndHeap.cpp | 4 +-- core/libs/tests/GuestStandardStreams.cpp | 42 +++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 5 files changed, 71 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index a3b839e5b8..41bb98efe0 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -789,8 +789,9 @@ if(BUILD_TESTING) add_test(NAME guest_standard_streams COMMAND guest_standard_stream_tests) add_test(NAME guest_standard_streams_closed_stdout COMMAND guest_standard_stream_tests --closed-stdout) add_test(NAME guest_standard_streams_exclusive_stdout COMMAND guest_standard_stream_tests --exclusive-stdout) + add_test(NAME guest_standard_streams_reopen_closed_stdout COMMAND guest_standard_stream_tests --reopen-closed-stdout) if(WIN32) - set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout guest_standard_streams_exclusive_stdout PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout guest_standard_streams_exclusive_stdout guest_standard_streams_reopen_closed_stdout PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() add_test_executable(guest_gets_tests ${LIBS_INCLUDE_DIR}/tests/GuestGets.cpp) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index a97f626f49..a6eb572afb 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -145,8 +145,31 @@ public: return directory; } + bool Adopt(std::FILE* handle) { + _handle = handle; + _guest = {}; + guestError = false; + randomDevice = false; + if (!_handle) return false; + _guest.flags = 0x10; +#ifdef _WIN32 + const int descriptor = _fileno(_handle); +#else + const int descriptor = ::fileno(_handle); +#endif + _guest.descriptor = descriptor >= 0 && descriptor <= 32767 ? static_cast<std::int16_t>(descriptor) : -1; + return true; + } + bool Reopen(const std::filesystem::path& filename, const char* mode, [[maybe_unused]] std::FILE* (*open)(const std::filesystem::path&, const std::string&) = nullptr) { + if (!_handle) { + if (!open || filename.empty()) { + errno = ENOENT; + return false; + } + return Adopt(open(filename, mode)); + } auto* previous = GetHandle(); _guest = {}; guestError = false; @@ -206,15 +229,7 @@ public: _handle = std::freopen(filename.c_str(), mode, previous); if (kept >= 0) RestoreHostStandard(previous, kept); #endif - if (!_handle) return false; - _guest.flags = 0x10; -#ifdef _WIN32 - const int descriptor = _fileno(_handle); -#else - const int descriptor = ::fileno(_handle); -#endif - _guest.descriptor = descriptor >= 0 && descriptor <= 32767 ? static_cast<std::int16_t>(descriptor) : -1; - return true; + return Adopt(_handle); } void PublishStatus(bool endOfFile, bool error) { __atomic_store_n(&_guest.readRemaining, 0, __ATOMIC_RELAXED); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index e023b0eaf4..29cbb2888e 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -375,7 +375,7 @@ static FileStream* OpenDirectoryStream(const std::filesystem::path& path) { #endif FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream) { - if (!stream || !mode) { errno = 22; return nullptr; } + if (!stream || !mode || (!filename && !stream->IsOpen())) { errno = 22; return nullptr; } if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. const auto parsed = ParseFileMode(mode); if (!parsed) { errno = 22; return nullptr; } @@ -383,7 +383,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode std::filesystem::path path; if (const int error = *filename ? ResolvePathError_nid_no_patch(filename, path) : 2) { if (stream->IsHostStandard()) stream->Detach(); - else stream->Close(); + else if (stream->IsOpen()) stream->Close(); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 9925382594..6523e76795 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -570,6 +570,46 @@ static void CheckClosedStreamWrites() { Require(fclose_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); } +static std::string FileText(const std::string& filename) { + std::ifstream input(filename, std::ios::binary); + return std::string(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()); +} + +static void CheckClosedStreamReopen() { + const auto filename = "anyps5-closed-reopen-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + FileStream stream(std::tmpfile()); + Require(fclose_nid_postfix(&stream) == 0); + Require(freopen_nid_postfix("missing-closed-stream-dir/out.log", "w", &stream) == nullptr && *__error_nid_postfix() == 2); + Require(fputs_nid_postfix("text", &stream) == -1 && *__error_nid_postfix() == 9); + const std::string overlong(300, 'n'); + Require(freopen_nid_postfix(overlong.c_str(), "w", &stream) == nullptr && *__error_nid_postfix() == 63); + Require(fputs_nid_postfix("text", &stream) == -1 && *__error_nid_postfix() == 9); + Require(freopen_nid_postfix(nullptr, "w", &stream) == nullptr && *__error_nid_postfix() == 22); + Require(freopen_nid_postfix(filename.c_str(), "w", &stream) == &stream); + Require(fputs_nid_postfix("reopened\n", &stream) >= 0 && fflush_nid_postfix(&stream) == 0); + Require(fileno_nid_postfix(&stream) >= 0); + Require(freopen_nid_postfix(filename.c_str(), "r", &stream) == &stream); + Require(fgetc_nid_postfix(&stream) == 'r'); + Require(fclose_nid_postfix(&stream) == 0); + Require(FileText(filename) == "reopened\n"); + Require(std::filesystem::remove(filename)); +} + +static int CheckReopenedClosedStandardOutput() { + const auto filename = "anyps5-closed-stdout-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(fclose_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(printf_nid_postfix("%d", 1) == -1 && *__error_nid_postfix() == 9); + const std::string overlong(300, 'n'); + Require(freopen_nid_postfix(overlong.c_str(), "w", __stdoutp_nid_postfix) == nullptr && *__error_nid_postfix() == 63); + Require(printf_nid_postfix("%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(freopen_nid_postfix(filename.c_str(), "w", __stdoutp_nid_postfix) == __stdoutp_nid_postfix); + Require(printf_nid_postfix("%d\n", 1) == 2 && puts_nid_postfix("text") >= 0 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(fclose_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(FileText(filename) == "1\ntext\n"); + Require(std::filesystem::remove(filename)); + return 0; +} + static int CheckClosedStandardOutput() { Require(freopen_nid_postfix("missing-closed-stream-dir/out.log", "w", __stdoutp_nid_postfix) == nullptr); Require(printf_nid_postfix("%d", 1) == -1 && *__error_nid_postfix() == 9); @@ -627,7 +667,9 @@ static void CheckStandardStreamDeleteSharing() { int main(int argc, char** argv) { if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); if (argc > 1 && std::string_view(argv[1]) == "--exclusive-stdout") return CheckExclusiveStandardOutput(); + if (argc > 1 && std::string_view(argv[1]) == "--reopen-closed-stdout") return CheckReopenedClosedStandardOutput(); CheckClosedStreamWrites(); + CheckClosedStreamReopen(); CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 3 && ___mb_cur_max_nid_postfix() == 1); CheckFlushWhileReading(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index df8cc320f0..a002043fd2 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -142,6 +142,7 @@ - [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` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - in a payload process on a PS5 (firmware 13.60) a directory opens in a read mode (`r`, `rb`, `rw`) and `fread` returns its raw directory blocks (`/data`, `/system`) or fails with `EIO` and sets the error indicator (`/`); `freopen` onto a directory in a read mode succeeds too. Here reading a directory stream fails with `EISDIR` and sets the error indicator on every host, since the host's directory blocks are not the console's, and on Windows `freopen` onto a directory fails with `EACCES`. The other measured answers match: `EISDIR` for a directory in a write or update mode, `EEXIST` for `wx` on an existing file, `EINVAL` for an unknown or empty mode, and `ENOENT` for a missing file +- [`freopen` of a closed stream](../../../core/libs/prx/libc/include/FileStream.hpp) (libc) opens the file on a new host descriptor, so `fileno` after `fclose(stdout)` and `freopen(path, mode, stdout)` is whatever the host hands out, where FreeBSD 11.4's [freopen.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/stdio/freopen.c) takes the lowest free descriptor, usually 1 again. The stream object is kept and reopened as there; the error values (`ENOENT` for a missing path, `EBADF` on the stream until it succeeds) follow that code and are not measured on a console. - [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry - [Writes on an update stream](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows go through the UCRT, which drops a write that directly follows a read without `fflush`, `fseek`, `fsetpos` or `rewind` unless the read reached end of file: `fwrite` returns 0, and `fputc` and `fprintf` report success, without the error indicator. FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74) discards the read buffer and writes at the descriptor's offset, past a read-ahead of `st_blksize` bytes for a regular file, so where the console writes depends on its block size. Reads directly after writes follow FreeBSD's `__srefill` - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way From 4c28647f0a825d2fedd633398faf588eb643623e Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:34:54 +0100 Subject: [PATCH 415/702] fix(libc): answer EBADF to a null filename on a closed stream as the console does --- core/libs/prx/libc/src/FileAndHeap.cpp | 3 ++- core/libs/tests/GuestStandardStreams.cpp | 2 +- docs/dev/debt/libc.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 29cbb2888e..8afb11b640 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -375,7 +375,8 @@ static FileStream* OpenDirectoryStream(const std::filesystem::path& path) { #endif FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream) { - if (!stream || !mode || (!filename && !stream->IsOpen())) { errno = 22; return nullptr; } + if (!stream || !mode) { errno = 22; return nullptr; } + if (!filename && !stream->IsOpen()) { errno = 9; return nullptr; } if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. const auto parsed = ParseFileMode(mode); if (!parsed) { errno = 22; return nullptr; } diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 6523e76795..422844a291 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -584,7 +584,7 @@ static void CheckClosedStreamReopen() { const std::string overlong(300, 'n'); Require(freopen_nid_postfix(overlong.c_str(), "w", &stream) == nullptr && *__error_nid_postfix() == 63); Require(fputs_nid_postfix("text", &stream) == -1 && *__error_nid_postfix() == 9); - Require(freopen_nid_postfix(nullptr, "w", &stream) == nullptr && *__error_nid_postfix() == 22); + Require(freopen_nid_postfix(nullptr, "w", &stream) == nullptr && *__error_nid_postfix() == 9); Require(freopen_nid_postfix(filename.c_str(), "w", &stream) == &stream); Require(fputs_nid_postfix("reopened\n", &stream) >= 0 && fflush_nid_postfix(&stream) == 0); Require(fileno_nid_postfix(&stream) >= 0); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index a002043fd2..2725cf6a12 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -142,7 +142,7 @@ - [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` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - in a payload process on a PS5 (firmware 13.60) a directory opens in a read mode (`r`, `rb`, `rw`) and `fread` returns its raw directory blocks (`/data`, `/system`) or fails with `EIO` and sets the error indicator (`/`); `freopen` onto a directory in a read mode succeeds too. Here reading a directory stream fails with `EISDIR` and sets the error indicator on every host, since the host's directory blocks are not the console's, and on Windows `freopen` onto a directory fails with `EACCES`. The other measured answers match: `EISDIR` for a directory in a write or update mode, `EEXIST` for `wx` on an existing file, `EINVAL` for an unknown or empty mode, and `ENOENT` for a missing file -- [`freopen` of a closed stream](../../../core/libs/prx/libc/include/FileStream.hpp) (libc) opens the file on a new host descriptor, so `fileno` after `fclose(stdout)` and `freopen(path, mode, stdout)` is whatever the host hands out, where FreeBSD 11.4's [freopen.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/stdio/freopen.c) takes the lowest free descriptor, usually 1 again. The stream object is kept and reopened as there; the error values (`ENOENT` for a missing path, `EBADF` on the stream until it succeeds) follow that code and are not measured on a console. +- [`freopen` of a closed stream](../../../core/libs/prx/libc/include/FileStream.hpp) (libc) opens the file on a new host descriptor, so `fileno` after `fclose(stdout)` and `freopen(path, mode, stdout)` is whatever the host hands out, where a PS5 takes the lowest free descriptor. The stream object is kept and reopened as there, and a missing path (`ENOENT`), an overlong one (`ENAMETOOLONG`) and a null filename (`EBADF`) answer as measured on a console. A write to the closed stream answers `EBADF` here, where the console's `fputs` and `fputc` return -1 leaving `errno` unchanged and its `fprintf` gives `EINVAL`. - [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry - [Writes on an update stream](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows go through the UCRT, which drops a write that directly follows a read without `fflush`, `fseek`, `fsetpos` or `rewind` unless the read reached end of file: `fwrite` returns 0, and `fputc` and `fprintf` report success, without the error indicator. FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74) discards the read buffer and writes at the descriptor's offset, past a read-ahead of `st_blksize` bytes for a regular file, so where the console writes depends on its block size. Reads directly after writes follow FreeBSD's `__srefill` - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way From ac682a406413a54a4cbd6e20cdc5616efaf89582 Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:22:37 +0000 Subject: [PATCH 416/702] fix(libSceFont): stop a font writing from owning its string's characters sceFontWritingInit took a reference on the string's host character storage and kept it in the caller's SceFontWriting bytes. Only a later sceFontWritingInit on the same, unchanged bytes dropped it, so every string drawn with a fresh, zeroed or overwritten writing leaked its characters, and re-initialising both copies of a copied writing dropped its one reference twice. A SceFontWriting has no teardown export, so it is plain caller memory and cannot own anything. sceFontCreateString now allocates the characters through the string's FontMemory, as it already did the string, so no character pointer the guest receives points into host memory. sceFontDestroyString clears the string's magic and frees both, and a failed create clears it before freeing the string. A writing keeps only a pointer to its string and checks the magic on every step, so after the destroy its steps return null unless that memory holds the magic again, as when a new string is allocated there. An init that fails after resetting the writing still leaves it stepping nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceFont/src/Text.cpp | 76 +++++------- core/libs/tests/GuestFont.cpp | 161 ++++++++++++++++++++++++++ docs/dev/debt/libSceFont.md | 1 + 3 files changed, 191 insertions(+), 47 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Text.cpp b/core/libs/prx/libSceFont/src/Text.cpp index 0b6823f3b3..21c7596e18 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -3,7 +3,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> -#include <atomic> #include <cstddef> #include <cstring> #include <new> @@ -31,11 +30,6 @@ constexpr std::int32_t WRITING_MASK_NONE = 0; constexpr std::int32_t WRITING_MASK_FORMAT_CHARACTERS = 1; constexpr std::uint64_t CHARACTER_FLAG_FORMAT = 1ull << 42; -struct CharacterStorage { - std::atomic<std::uint32_t> refCount{1}; - std::vector<FontTextCharacter> characters; -}; - struct FontStringData { std::uint16_t magic = FONT_STRING_MAGIC; std::uint8_t flags = 0; @@ -48,7 +42,7 @@ struct FontStringData { void* terminateOrder = nullptr; std::uint32_t textCount = 0; std::uint32_t renderCount = 0; - CharacterStorage* storage = nullptr; + FontTextCharacter* characters = nullptr; }; struct FontWritingData { @@ -56,7 +50,6 @@ struct FontWritingData { std::uint16_t reserved0 = 0; std::uint32_t writingForm = WRITING_FORM_HORIZONTAL; FontString string = nullptr; - CharacterStorage* storage = nullptr; FontHandle defaultFont = nullptr; std::size_t nextIndex = 0; std::size_t currentIndex = 0; @@ -66,7 +59,6 @@ struct FontWritingData { bool hasCurrentStep = false; bool hasCurrentCharacter = false; bool maskFormatCharacters = true; - bool ownsStorage = false; bool hasRawGlyphMetrics = false; FontWritingStep step{}; FontWritingMetrics metrics{}; @@ -87,18 +79,6 @@ FontWritingData* GetWritingData(FontWriting* fontWriting) { return data->magic == FONT_WRITING_MAGIC ? data : nullptr; } -void ReleaseStorage(CharacterStorage*& storage) { - if (!storage) return; - if (storage->refCount.fetch_sub(1, std::memory_order_acq_rel) == 1) delete storage; - storage = nullptr; -} - -void ReleaseWritingStorage(FontWritingData& writing) { - if (!writing.ownsStorage) return; - ReleaseStorage(writing.storage); - writing.ownsStorage = false; -} - std::uint16_t TextSourceMagic(const FontTextSource* source) { return static_cast<std::uint16_t>(source->systemUse0 & 0xFFFFu); } @@ -135,15 +115,15 @@ FontTextCharacter MakeCharacter(FontHandle font, std::uint32_t code, void* textO return character; } -void LinkCharacters(std::vector<FontTextCharacter>& characters) { - for (std::size_t i = 0; i < characters.size(); ++i) { +void LinkCharacters(FontTextCharacter* characters, std::size_t count) { + for (std::size_t i = 0; i < count; ++i) { characters[i].prev = i == 0 ? nullptr : &characters[i - 1]; - characters[i].next = i + 1 < characters.size() ? &characters[i + 1] : nullptr; + characters[i].next = i + 1 < count ? &characters[i + 1] : nullptr; } } FontTextCharacter* TextCharacters(FontStringData* data) { - return data && data->storage && !data->storage->characters.empty() ? data->storage->characters.data() : nullptr; + return data && data->textCount != 0 ? data->characters : nullptr; } bool IsRightToLeftLetter(std::uint32_t code) { @@ -194,8 +174,9 @@ void FillWritingMetricsFromGlyph(FontWritingData& writing, const FontGlyphMetric } bool PopulateWritingStep(FontWritingData& writing, std::size_t index) { - if (!writing.storage || index >= writing.storage->characters.size()) return false; - const FontTextCharacter& character = writing.storage->characters[index]; + const FontStringData* data = GetStringData(writing.string); + if (!data || index >= data->textCount) return false; + const FontTextCharacter& character = data->characters[index]; const FontHandle font = character.font ? character.font : writing.defaultFont; writing.step.x = writing.penX; writing.step.y = writing.penY; @@ -328,9 +309,9 @@ int APS5_VABI sceFontCreateString(const FontMemory* fontMemory, FontTextSource* data->memory = fontMemory; data->source = textSource; data->defaultFont = textSource->defaultFont; - data->storage = new CharacterStorage{}; if (!data->defaultFont && detail) data->defaultFont = detail->defaultFont; data->writingForm = static_cast<std::uint32_t>(textSource->systemUse0 >> 32); + std::vector<FontTextCharacter> characters; std::int32_t state = SCE_FONT_ERROR_INVALID_TEXT_SOURCE; void* textOrder = nullptr; for (std::size_t parsed = 0; parsed < MAX_PARSED_CHARACTERS; ++parsed) { @@ -342,7 +323,7 @@ int APS5_VABI sceFontCreateString(const FontMemory* fontMemory, FontTextSource* state = SCE_FONT_ERROR_INVALID_TEXT_SOURCE; break; } - data->storage->characters.push_back(MakeCharacter(font, result.FontCode.code, textOrder)); + characters.push_back(MakeCharacter(font, result.FontCode.code, textOrder)); continue; } if (state == PARSER_RESULT_TERMINATE) { @@ -357,15 +338,19 @@ int APS5_VABI sceFontCreateString(const FontMemory* fontMemory, FontTextSource* break; } if (state == PARSER_RESULT_FONT_CODE) state = SCE_FONT_ERROR_INVALID_TEXT_SOURCE; + if (state == SCE_FONT_OK && !characters.empty()) { + data->characters = static_cast<FontTextCharacter*>(fontMemory->iface->alloc(fontMemory->mspace_handle, static_cast<std::uint32_t>(characters.size() * sizeof(FontTextCharacter)))); + if (!data->characters) state = SCE_FONT_ERROR_ALLOCATION_FAILED; + } if (state != SCE_FONT_OK) { - ReleaseStorage(data->storage); - data->~FontStringData(); + data->magic = 0; fontMemory->iface->dealloc(fontMemory->mspace_handle, raw); return state; } - if (data->writingForm == WRITING_FORM_HORIZONTAL) data->writingForm = DefaultStringForm(data->storage->characters); - LinkCharacters(data->storage->characters); - data->textCount = static_cast<std::uint32_t>(data->storage->characters.size()); + if (data->writingForm == WRITING_FORM_HORIZONTAL) data->writingForm = DefaultStringForm(characters); + std::copy(characters.begin(), characters.end(), data->characters); + LinkCharacters(data->characters, characters.size()); + data->textCount = static_cast<std::uint32_t>(characters.size()); data->renderCount = data->textCount; *pFontString = reinterpret_cast<FontString>(data); return SCE_FONT_OK; @@ -379,8 +364,8 @@ int APS5_VABI sceFontDestroyString(FontString* pFontString) { return SCE_FONT_ERROR_INVALID_STRING; } const FontMemory* memory = data->memory; - ReleaseStorage(data->storage); - data->~FontStringData(); + data->magic = 0; + if (data->characters) memory->iface->dealloc(memory->mspace_handle, data->characters); memory->iface->dealloc(memory->mspace_handle, data); *pFontString = nullptr; return SCE_FONT_OK; @@ -640,25 +625,21 @@ int APS5_VABI sceFontWritingInit(FontWriting* fontWriting, FontString fontString if (!fontWriting || !fontString || !fontCharacter) return SCE_FONT_ERROR_INVALID_PARAMETER; auto* data = GetStringData(fontString); if (!data) return SCE_FONT_ERROR_INVALID_STRING; - if (auto* previous = GetWritingData(fontWriting)) ReleaseWritingStorage(*previous); std::memset(fontWriting, 0, sizeof(*fontWriting)); auto* writing = new (fontWriting->systemUse) FontWritingData{}; writing->string = fontString; - writing->storage = data->storage; writing->defaultFont = data->defaultFont; - if (writing->storage) writing->storage->refCount.fetch_add(1, std::memory_order_relaxed); - writing->ownsStorage = true; writing->writingForm = data->writingForm; const FontTextCharacter* characters = TextCharacters(data); - if (!characters || fontCharacter < characters || fontCharacter >= characters + data->storage->characters.size()) { - ReleaseWritingStorage(*writing); + if (!characters || fontCharacter < characters || fontCharacter >= characters + data->textCount) { + writing->string = nullptr; return SCE_FONT_ERROR_INVALID_PARAMETER; } writing->nextIndex = static_cast<std::size_t>(fontCharacter - characters); writing->currentIndex = writing->nextIndex; writing->hasCurrentStep = false; if (!PopulateWritingStep(*writing, writing->nextIndex)) { - ReleaseWritingStorage(*writing); + writing->string = nullptr; return SCE_FONT_ERROR_INVALID_PARAMETER; } writing->penX = 0.0f; @@ -684,8 +665,7 @@ int APS5_VABI sceFontWritingGetRenderMetrics(FontWriting* fontWriting, FontWriti const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* fontWriting) { auto* writing = GetWritingData(fontWriting); - if (!writing || !writing->storage || writing->nextIndex >= writing->storage->characters.size()) return nullptr; - if (!PopulateWritingStep(*writing, writing->nextIndex)) return nullptr; + if (!writing || !PopulateWritingStep(*writing, writing->nextIndex)) return nullptr; writing->currentIndex = writing->nextIndex; writing->hasCurrentStep = true; writing->hasCurrentCharacter = true; @@ -697,7 +677,9 @@ const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* fon FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, const FontWritingLetterStep** pLetterStep) { if (pLetterStep) *pLetterStep = nullptr; auto* writing = GetWritingData(fontWriting); - if (!writing || !writing->hasCurrentStep || !writing->hasCurrentCharacter || !writing->storage || writing->currentIndex >= writing->storage->characters.size()) return nullptr; + if (!writing || !writing->hasCurrentStep || !writing->hasCurrentCharacter) return nullptr; + auto* data = GetStringData(writing->string); + if (!data || writing->currentIndex >= data->textCount) return nullptr; writing->hasCurrentCharacter = false; auto& letterStep = writing->letterStep; letterStep = {}; @@ -711,7 +693,7 @@ FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting letterStep.Components.glyphsIndex = static_cast<std::uint32_t>(writing->currentIndex); letterStep.Components.characterTextCount = static_cast<std::uint8_t>(std::max<std::uint32_t>(writing->step.Profile.characterCount, 1)); if (pLetterStep) *pLetterStep = &letterStep; - return &writing->storage->characters[writing->currentIndex]; + return &data->characters[writing->currentIndex]; } int APS5_VABI sceFontWritingSetMaskInvisible(FontWriting* fontWriting, std::int32_t mask) { diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 093c88fe45..297cb35c78 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -18,6 +18,9 @@ #include <stdexcept> #include <string> #include <vector> +#ifdef __GLIBC__ +#include <malloc.h> +#endif static void Check(bool value, int line) { if (!value) { @@ -45,6 +48,41 @@ static void APS5_VABI Release(void*, void* pointer) { std::free(pointer); } +struct HeldBlock { + void* pointer; + std::uint32_t size; + bool released; +}; + +static std::vector<HeldBlock> heldBlocks; +static std::size_t heldBlockLimit = SIZE_MAX; + +static void* APS5_VABI HoldAllocate(void*, std::uint32_t size) { + if (heldBlocks.size() >= heldBlockLimit) return nullptr; + void* pointer = std::malloc(size); + heldBlocks.push_back({pointer, size, false}); + return pointer; +} + +static void APS5_VABI HoldRelease(void*, void* pointer) { + for (HeldBlock& block : heldBlocks) { + if (block.pointer == pointer && !block.released) { + block.released = true; + return; + } + } + Require(false); +} + +static const HeldBlock* HeldBlockOf(const void* pointer) { + const auto address = reinterpret_cast<std::uintptr_t>(pointer); + for (const HeldBlock& block : heldBlocks) { + const auto begin = reinterpret_cast<std::uintptr_t>(block.pointer); + if (address >= begin && address - begin < block.size) return █ + } + return nullptr; +} + static std::uint32_t APS5_VABI CoarsePixelResolution() { return 16; } @@ -106,6 +144,127 @@ static std::int32_t APS5_VABI ParseCodes(FontTextSource* source, void** order, F return 1; } +static void CheckWritingDoesNotOwnCharacters(const FontMemory* memory, FontHandle font) { + const std::string text(32, 'A'); + ParsedText parsed{text.c_str(), 0, font}; + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, parsed.text, static_cast<std::uint32_t>(text.size()), ParseText, &parsed) == SCE_FONT_OK); + FontTextCharacter* characters = nullptr; + const auto create = [&] { + parsed.at = 0; + FontString string = nullptr; + Require(sceFontCreateString(memory, &source, nullptr, &string) == SCE_FONT_OK); + std::uint32_t count = 0; + characters = sceFontStringRefersTextCharacters(string, &count); + Require(characters != nullptr && count == text.size()); + return string; + }; + const auto steps = [](FontWriting* writing) { + std::size_t count = 0; + while (sceFontWritingRefersRenderStep(writing) != nullptr) ++count; + return count; + }; +#ifdef __GLIBC__ + const auto before = mallinfo2().uordblks; +#endif + const int allocationsBefore = allocations; + for (int i = 0; i < 512; ++i) { + FontString string = create(); + FontWriting writing{}; + Require(sceFontWritingInit(&writing, string, characters) == SCE_FONT_OK && steps(&writing) == text.size()); + Require(sceFontDestroyString(&string) == SCE_FONT_OK); + } + Require(allocations == allocationsBefore); +#ifdef __GLIBC__ + Require(static_cast<long long>(mallinfo2().uordblks) - static_cast<long long>(before) < 256 * 1024); +#endif + FontString string = create(); + FontWriting first{}; + Require(sceFontWritingInit(&first, string, characters) == SCE_FONT_OK); + FontWriting copy; + std::memcpy(©, &first, sizeof(copy)); + Require(steps(&first) == text.size() && steps(©) == text.size()); + Require(sceFontDestroyString(&string) == SCE_FONT_OK); + string = create(); + Require(sceFontWritingInit(&first, string, characters) == SCE_FONT_OK && sceFontWritingInit(©, string, characters) == SCE_FONT_OK); + Require(steps(&first) == text.size() && steps(©) == text.size()); + Require(sceFontDestroyString(&string) == SCE_FONT_OK); + FontString other = create(); + const FontTextCharacter* otherCharacters = characters; + string = create(); + FontWriting failed{}; + Require(sceFontWritingInit(&failed, string, characters) == SCE_FONT_OK); + Require(sceFontWritingInit(&failed, string, otherCharacters) == SCE_FONT_ERROR_INVALID_PARAMETER && sceFontWritingRefersRenderStep(&failed) == nullptr); + Require(sceFontDestroyString(&string) == SCE_FONT_OK && sceFontDestroyString(&other) == SCE_FONT_OK); +} + +static void CheckWritingAfterStringDestroyed(FontHandle font) { + const FontMemoryInterface iface{HoldAllocate, HoldRelease, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); + const std::string text(32, 'A'); + ParsedText parsed{text.c_str(), 0, font}; + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, parsed.text, static_cast<std::uint32_t>(text.size()), ParseText, &parsed) == SCE_FONT_OK); + const auto create = [&](FontTextCharacter** characters) { + parsed.at = 0; + FontString string = nullptr; + Require(sceFontCreateString(&memory, &source, nullptr, &string) == SCE_FONT_OK && HeldBlockOf(string) != nullptr); + std::uint32_t count = 0; + *characters = sceFontStringRefersTextCharacters(string, &count); + Require(*characters != nullptr && count == text.size()); + Require(HeldBlockOf(*characters) != nullptr && HeldBlockOf(*characters + count - 1) == HeldBlockOf(*characters)); + return string; + }; + const auto steps = [](FontWriting* writing) { + std::size_t count = 0; + while (sceFontWritingRefersRenderStep(writing) != nullptr) ++count; + return count; + }; + const FontWritingLetterStep unset{}; + const FontWritingLetterStep* letter = nullptr; + FontTextCharacter* characters = nullptr; + parsed.at = 0; + FontString string = nullptr; + const std::size_t blocksBefore = heldBlocks.size(); + heldBlockLimit = blocksBefore + 1; + Require(sceFontCreateString(&memory, &source, nullptr, &string) == SCE_FONT_ERROR_ALLOCATION_FAILED && string == nullptr); + heldBlockLimit = SIZE_MAX; + Require(heldBlocks.size() == blocksBefore + 1 && heldBlocks.back().released); + FontString failedString = static_cast<FontString>(heldBlocks.back().pointer); + Require(sceFontDestroyString(&failedString) == SCE_FONT_ERROR_INVALID_STRING && failedString == nullptr); + string = create(&characters); + FontString staleHandle = string; + const void* destroyed = string; + FontWriting writing{}; + Require(sceFontWritingInit(&writing, string, characters) == SCE_FONT_OK && sceFontWritingRefersRenderStep(&writing) != nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &letter) == characters && letter != nullptr); + Require(sceFontWritingRefersRenderStep(&writing) != nullptr); + FontWriting copy; + std::memcpy(©, &writing, sizeof(copy)); + Require(sceFontDestroyString(&string) == SCE_FONT_OK && string == nullptr); + Require(HeldBlockOf(destroyed)->released && HeldBlockOf(characters)->released); + for (FontWriting* stale : {&writing, ©}) { + letter = &unset; + Require(sceFontWritingRefersRenderStepCharacter(stale, &letter) == nullptr && letter == nullptr); + Require(sceFontWritingRefersRenderStep(stale) == nullptr); + } + Require(sceFontWritingInit(©, staleHandle, characters) == SCE_FONT_ERROR_INVALID_STRING && sceFontWritingRefersRenderStep(©) == nullptr); + Require(sceFontDestroyString(&staleHandle) == SCE_FONT_ERROR_INVALID_STRING && staleHandle == nullptr); + FontTextCharacter* fresh = nullptr; + string = create(&fresh); + Require(sceFontWritingInit(&writing, string, fresh + 1) == SCE_FONT_OK && sceFontWritingInit(©, string, fresh) == SCE_FONT_OK); + Require(steps(&writing) == text.size() - 1 && sceFontWritingRefersRenderStepCharacter(&writing, &letter) == fresh + text.size() - 1); + Require(steps(©) == text.size()); + Require(sceFontDestroyString(&string) == SCE_FONT_OK); + for (const HeldBlock& block : heldBlocks) { + Require(block.released); + std::free(block.pointer); + } + heldBlocks.clear(); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); +} + static void CheckTextCodes(const FontMemory* memory, FontHandle font) { ParsedText parsed{"AVA", 0, font}; FontTextSource source{}; @@ -697,6 +856,8 @@ int main() { Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters + 1 && out.letter == retainedLetter && out.guard == guard); Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr); Require(sceFontDestroyString(&string) == SCE_FONT_OK); + CheckWritingDoesNotOwnCharacters(&memory, font); + CheckWritingAfterStringDestroyed(font); CheckTextCodes(&memory, font); Require(sceFontCloseFont(font) == SCE_FONT_OK); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 1b5466bbaf..a0f4c548a1 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -32,6 +32,7 @@ - [sceFontGetFontMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 64-byte output, the form fallbacks (2 and 3 fall back to the `hhea` values when OS/2 is missing or gives an empty range, 4 builds vertical values from `vhea` or OS/2) and the raw weight deltas and slant copied from the style are not measured on a console. For a font set the values are those of its primary file; which file the console reports is not recovered. Closes when: measured on a PS5 - [sceFontGetFontStyleInformation](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 24-byte output (FreeType family and style names, italic and bold flags, OS/2 weight class when it is a multiple of 100 from 100 to 900, width class below 10, and 1 plus a right-to-left `head` direction plus 0x80 for vertical metrics) and the font-set record path are not measured on a console. A face without a `head` table, such as a BDF font, throws, as its console result is unknown. Closes when: measured on a PS5 - [sceFontGetFontGlyphsOutlineProfile](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 16-byte output (the `maxp` maximum contours, points, composite contours and composite points) and `SCE_FONT_ERROR_NO_SUPPORT_FUNCTION` for a `maxp` older than 1.0 are not measured on a console. A handle with a font-set record throws, as the maximum over the record's system font entries is not modelled. A face without a `maxp` table throws, as its console result is unknown. Closes when: measured on a PS5 +- [sceFontWritingRefersRenderStep and sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp) on a writing whose string was destroyed: what the console returns there and on a second destroy, and where it allocates a string's characters, is not measured. Here [sceFontCreateString](../../../core/libs/prx/libSceFont/src/Text.cpp) allocates the characters through the string's `FontMemory`, as it does the string, and [sceFontDestroyString](../../../core/libs/prx/libSceFont/src/Text.cpp) clears the string's magic and frees both. A writing keeps only a pointer to its string and checks the magic on each step, so both calls return null after the destroy unless that memory holds the magic again, as when a new string is allocated there. A second destroy through another copy of the handle returns `SCE_FONT_ERROR_INVALID_STRING` unless the magic is back. Character pointers taken from the string then point into freed guest memory. Closes when: measured on a PS5 - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. - [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. From ac7aa18e5f8bc07adfdbda182665f0768209698c Mon Sep 17 00:00:00 2001 From: BiladeiroSupremo <ag.rio@hotmail.com> Date: Sun, 18 Oct 2026 07:46:36 -0300 Subject: [PATCH 417/702] perf(agc): reuse context register storage across context state push, pop and clear --- .../Execution/include/QueueState.hpp | 31 +++++++++++++++++-- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 5 ++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index 7c10d006f5..a37ae3a4d1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -251,11 +251,26 @@ inline Registers BuildInitialContextRegisters() { return result; } -inline Registers InitialContextRegisters() { +inline const Registers& InitialContextRegisterSet() { static const Registers& initial = *new Registers(BuildInitialContextRegisters()); return initial; } +inline Registers InitialContextRegisters() { + return InitialContextRegisterSet(); +} + +// Storage a pushed context state returns to when it is popped, so the next push copies into +// buffers it already has. A copied queue state starts without one. +struct SpareRegisters { + Registers registers; + SpareRegisters() = default; + SpareRegisters(const SpareRegisters&) {} + SpareRegisters& operator=(const SpareRegisters&) { return *this; } + SpareRegisters(SpareRegisters&&) noexcept = default; + SpareRegisters& operator=(SpareRegisters&&) noexcept = default; +}; + struct Predication { std::uint64_t address = 0; std::uint32_t operation = 0; @@ -268,6 +283,7 @@ struct QueueState { Registers context = InitialContextRegisters(); Registers userConfig{{0x24a, 0}, {0x24b, 0}}; std::optional<Registers> savedContext; + SpareRegisters spareContext; std::vector<std::uint32_t> constantRam; std::uint64_t indexBase = 0; std::uint64_t drawIndirectBase = 0; @@ -279,7 +295,18 @@ struct QueueState { Predication predication; void ClearContext() { - context = InitialContextRegisters(); + context = InitialContextRegisterSet(); + } + + void PushContext() { + spareContext.registers = context; + savedContext.emplace(std::move(spareContext.registers)); + } + + void PopContext() { + std::swap(context, *savedContext); + spareContext.registers = std::move(*savedContext); + savedContext.reset(); } std::span<const std::uint32_t> ConstantRam() const { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 6a78d16662..a4e136fdcf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -1034,13 +1034,12 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) { case 0: queue.ClearContext(); break; case 1: case 3: require(!queue.savedContext.has_value(), "context state is already pushed"); - queue.savedContext = queue.context; + queue.PushContext(); if (packet[1] == 3) queue.ClearContext(); break; case 2: require(queue.savedContext.has_value(), "context state has not been pushed"); - queue.context = std::move(*queue.savedContext); - queue.savedContext.reset(); + queue.PopContext(); break; } APS5_LOG_OUT_DEBUG("CONTEXT_STATE done operation=%u targetMaskAfter=0x%x shaderMaskAfter=0x%x", packet[1], queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u); From eff427b899d2f12dedfe94b2de2afab164bc296a Mon Sep 17 00:00:00 2001 From: BiladeiroSupremo <ag.rio@hotmail.com> Date: Sun, 18 Oct 2026 06:53:28 -0300 Subject: [PATCH 418/702] perf(agc): keep a shadow's live unit count as units change instead of recounting every unit --- core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp index cd5212cf8f..e1c4285c92 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp @@ -378,12 +378,14 @@ std::size_t publishUnits(const std::shared_ptr<UnitShadow>& shadow, std::uint64_ std::lock_guard lock(registry.mutex); std::vector<std::uint8_t> changed, cpu; changedUnits(*shadow, first, last, changed, cpu); + std::uint32_t ended = 0; for (auto unit = first; unit <= last; ++unit) { const auto k = static_cast<std::size_t>(unit - first); if (!selected[k] || !shadow->Live(unit)) continue; if (changed[k] == GuestMemory::BlockWritten) { // Newer bytes reached the import since the retile: the slab's are dead. shadow->generation[unit] = 0; + ++ended; (cpu[k] != 0 ? Stats().staleDroppedCpu : Stats().staleDroppedDriver).fetch_add(1, std::memory_order_relaxed); continue; } @@ -391,6 +393,7 @@ std::size_t publishUnits(const std::shared_ptr<UnitShadow>& shadow, std::uint64_ const auto& slab = shadow->slabs[slabIndex]; if (slab == nullptr) { shadow->generation[unit] = 0; + ++ended; continue; } const auto [begin, end] = shadow->UnitRange(unit); @@ -405,11 +408,12 @@ std::size_t publishUnits(const std::shared_ptr<UnitShadow>& shadow, std::uint64_ ranges.emplace_back(begin, end); } shadow->published[unit] = 1; + ++ended; slab->lastUse = Recorder::Presents(); bytes += end - begin; ++units; } - shadow->Recount(); + shadow->liveUnits -= ended; } if (units == 0) return 0; Stats().published[static_cast<std::size_t>(reason)].fetch_add(1, std::memory_order_relaxed); @@ -800,12 +804,12 @@ void MarkShadowed(const HostImport& import, std::span<const ShadowedRange> range ++lost; continue; } + if (!shadow->Live(unit)) ++shadow->liveUnits; shadow->generation[unit] = generation; shadow->published[unit] = 0; current->lastUse = present; } } - shadow->Recount(); } if (lost != 0) { Stats().slabLost.fetch_add(lost, std::memory_order_relaxed); From 7054872694e9d1cc130962ffaf406bd67371cbca Mon Sep 17 00:00:00 2001 From: Alexandre Figueiredo <alexfigas11@gmail.com> Date: Tue, 27 Oct 2026 00:31:16 +0000 Subject: [PATCH 419/702] feat(libSceNpCppWebApi): track the SessionManager V1 parameter lifecycles as the module does --- core/libs/prx/libSceNpCppWebApi/Export.cpp | 64 ++++++++++------- .../prx/libSceNpCppWebApi/include/Api.hpp | 32 ++++----- core/libs/tests/GuestNpCppWebApiOffline.cpp | 71 +++++++++++++++++-- docs/dev/debt/libSceNpCppWebApi.md | 2 +- 4 files changed, 124 insertions(+), 45 deletions(-) diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index c8e9a9ca52..340eb7ffed 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -208,6 +208,7 @@ static constexpr int ParameterNotInitialized = static_cast<int>(0x80553502); static constexpr int ParameterInvalidArgument = static_cast<int>(0x80553503); static constexpr int ParameterAlreadyInitialized = static_cast<int>(0x8055350f); static constexpr int ParameterTerminatedUninitialized = static_cast<int>(0x80553510); +static constexpr int ParameterBodyMissing = static_cast<int>(0x80553512); struct ParameterObject { void* base; @@ -242,6 +243,13 @@ static int TerminateParameter(void* self) { return 0; } +static int InitializeParameterWithBody(void* self, const std::uint64_t* context, const bool textsPresent, const IntrusivePtr* body) { + const int result = InitializeParameter(self, context, textsPresent); + if (result != 0 || body->object != nullptr) return result; + static_cast<ParameterObject*>(self)->implementation = nullptr; + return ParameterBodyMissing; +} + int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci(void* self, const std::uint64_t* context, const char* text, int value) { (void)value; return InitializeParameter(self, context, text != nullptr); @@ -967,7 +975,8 @@ void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sess return nullptr; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerD1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerD1Ev(void* self) { + (void)self; return 0; } @@ -977,8 +986,8 @@ const char* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V110FromMember11getO return nullptr; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations26setinvitationInvalidFilterEPKc() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations26setinvitationInvalidFilterEPKc(void* self, const char* filter) { + return SetParameterText(self, filter); } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEC1Ev(IntrusivePtr* self) { @@ -997,7 +1006,8 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 *self = {}; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionD1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionD1Ev(void* self) { + (void)self; return 0; } @@ -1126,12 +1136,12 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15DefaultResponseC1Ev(void* self) { return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer9terminateEv() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer9terminateEv(void* self) { + return TerminateParameter(self); } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_29PostPlayerSessionsRequestBodyEEE() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_29PostPlayerSessionsRequestBodyEEE(void* self, const std::uint64_t* context, const IntrusivePtr* body) { + return InitializeParameterWithBody(self, context, true, body); } void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V110FromMemberEEptEv(const IntrusivePtr* self) { @@ -1151,7 +1161,8 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sessi ReleaseIntrusivePtr(self, 0x30, __func__); } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionC1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionC1Ev(void* self) { + ConstructParameter(self); return 0; } @@ -1197,7 +1208,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsD1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsD1Ev(void* self) { + (void)self; return 0; } @@ -1233,7 +1245,8 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2 return self->object; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsC1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsC1Ev(void* self) { + ConstructParameter(self); return 0; } @@ -1260,7 +1273,8 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_1 return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsD1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsD1Ev(void* self) { + (void)self; return 0; } @@ -1292,12 +1306,13 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsC1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsC1Ev(void* self) { + ConstructParameter(self); return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_51PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEE() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_51PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body) { + return InitializeParameterWithBody(self, context, sessionId != nullptr, body); } bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBody16invitationsIsSetEv(const void* self) { @@ -1310,8 +1325,8 @@ void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sess return HostVectorElement(self, index, __func__); } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession10initializeEPNS1_6Common10LibContextEPKcSA_() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession10initializeEPNS1_6Common10LibContextEPKcSA_(void* self, const std::uint64_t* context, const char* sessionId, const char* accountId) { + return InitializeParameter(self, context, sessionId != nullptr && accountId != nullptr); } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V115LocalizedStringEED1Ev(IntrusivePtr* self) { @@ -1357,7 +1372,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi20create return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerC1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerC1Ev(void* self) { + ConstructParameter(self); return 0; } @@ -1412,8 +1428,8 @@ bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14Se return false; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKc() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* accountId) { + return InitializeParameter(self, context, accountId != nullptr); } void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEptEv(const IntrusivePtr* self) { @@ -2037,8 +2053,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28Parame return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession9terminateEv() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession9terminateEv(void* self) { + return TerminateParameter(self); } int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsD1Ev() { @@ -2067,8 +2083,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBloc return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions9terminateEv() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions9terminateEv(void* self) { + return TerminateParameter(self); } int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead16customData1IsSetEv() { diff --git a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp index b827506807..9730b22af1 100644 --- a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp @@ -179,14 +179,14 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2 int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession16setMaxSpectatorsERKi(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139RequestPlayerSessionMemberPlayerFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_32RequestCreatePlayerSessionPlayerEEEEEPNS9_INS3_32RequestPlayerSessionMemberPlayerEEE(); void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEE3endEv(void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerD1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerD1Ev(void* self); const char* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V110FromMember11getOnlineIdEv(const void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations26setinvitationInvalidFilterEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations26setinvitationInvalidFilterEPKc(void* self, const char* filter); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEC1Ev(IntrusivePtr* self); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEptEv(const IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V151PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEC1Ev(IntrusivePtr* self); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionD1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionD1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V122LocalizedStringFactory6createEPNS1_6Common10LibContextEPKcNS_4Json6ObjectEPNS5_12IntrusivePtrINS3_15LocalizedStringEEE(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V151PostPlayerSessionsSessionIdMemberPlayersRequestBodyEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEEED1Ev(IntrusivePtr* self); @@ -212,13 +212,13 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi25joinPl int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendEPKc(CommonString* self, const char* text); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15DefaultResponseC1Ev(void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer9terminateEv(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_29PostPlayerSessionsRequestBodyEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_29PostPlayerSessionsRequestBodyEEE(void* self, const std::uint64_t* context, const IntrusivePtr* body); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V110FromMemberEEptEv(const IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestPlayerSessionMemberPlayerEED1Ev(IntrusivePtr* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(IntrusivePtr* self); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionC1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionC1Ev(void* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEC1Ev(IntrusivePtr* self); const char* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead12getSessionIdEv(const void* self); @@ -228,7 +228,7 @@ void* APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V130PostPlayerSessionsRespo int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi18leavePlayerSessionEiRKNS4_29ParameterToLeavePlayerSessionERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsD1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsD1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi27getPlayerSessionInvitationsEiRKNS4_38ParameterToGetPlayerSessionInvitationsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_54GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestPlayerSessionMemberPlayerEEC1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEED1Ev(IntrusivePtr* self); @@ -237,24 +237,24 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEC1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V110FromMemberEED1Ev(IntrusivePtr* self); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEptEv(const IntrusivePtr* self); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsC1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element); void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEppEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession19setJoinableUserTypeERKNS3_16JoinableUserTypeE(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsD1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsD1Ev(void* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestPlayerSessionMemberPlayerEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead14sessionIdIsSetEv(const void* self); bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead17invitationIdIsSetEv(const void* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsC1Ev(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_51PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_51PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body); bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBody16invitationsIsSetEv(const void* self); void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEEEixEm(void* self, std::uint64_t index); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession10initializeEPNS1_6Common10LibContextEPKcSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession10initializeEPNS1_6Common10LibContextEPKcSA_(void* self, const std::uint64_t* context, const char* sessionId, const char* accountId); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V115LocalizedStringEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEdeEv(const void* self); @@ -264,7 +264,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6String void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V133RequestPlayerSessionPlayerFactory6createEPNS1_6Common10LibContextEPKcS9_PNS5_12IntrusivePtrINS3_26RequestPlayerSessionPlayerEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi20createPlayerSessionsEiRKNS4_31ParameterToCreatePlayerSessionsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_30PostPlayerSessionsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerC1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerC1Ev(void* self); bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead9fromIsSetEv(const void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V126RequestPlayerSessionPlayer15setPushContextsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_24PlayerSessionPushContextEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V127RequestPlayerSessionFactory6createEPNS1_6Common10LibContextEiNS5_12IntrusivePtrINS3_32RequestPlayerSessionMemberPlayerEEERKNS5_6VectorINS5_6StringEEENS8_INS3_15LocalizedStringEEEPNS8_INS3_20RequestPlayerSessionEEE(); @@ -276,7 +276,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEC1Ev(IntrusivePtr* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(IntrusivePtr* self, const void* element); bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEneERKS9_(const void* self, const void* other); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* accountId); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEptEv(const IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(IntrusivePtr* self); @@ -408,14 +408,14 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V127ResponsePlayerSessionPla int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V125GetBasicPresencesResponse17getBasicPresencesEv(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9setfieldsENS1_6Common12IntrusivePtrINS6_6VectorINS6_6StringEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9terminateEv(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession9terminateEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsD1Ev(); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead14getCustomData1Ev(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev(void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions9terminateEv(void* self); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead16customData1IsSetEv(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38addPlayerSessionJoinableSpecifiedUsersEiRKNS4_49ParameterToAddPlayerSessionJoinableSpecifiedUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_61PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead9getLeaderEv(); diff --git a/core/libs/tests/GuestNpCppWebApiOffline.cpp b/core/libs/tests/GuestNpCppWebApiOffline.cpp index 4ffa0c8e76..6fcbf72a07 100644 --- a/core/libs/tests/GuestNpCppWebApiOffline.cpp +++ b/core/libs/tests/GuestNpCppWebApiOffline.cpp @@ -20,6 +20,7 @@ static constexpr int ParameterNotInitialized = static_cast<int>(0x80553502); static constexpr int ParameterInvalidArgument = static_cast<int>(0x80553503); static constexpr int ParameterAlreadyInitialized = static_cast<int>(0x8055350f); static constexpr int ParameterTerminatedUninitialized = static_cast<int>(0x80553510); +static constexpr int ParameterBodyMissing = static_cast<int>(0x80553512); struct ParameterLifecycle { int (APS5_VABI* construct)(void*); @@ -36,17 +37,17 @@ static void TestParameterLifecycle(const ParameterLifecycle& lifecycle, TInitial Require(lifecycle.construct(parameter) == 0, "a parameter constructor failed"); for (std::size_t index = 0; index < sizeof(parameter); ++index) Require(parameter[index] == (index < 0x10 ? 0 : 0x5a), "a parameter constructor wrote the wrong bytes"); - Require(lifecycle.setText(parameter, "text") == ParameterNotInitialized, "a setter accepted an uninitialized parameter"); + Require(lifecycle.setText == nullptr || lifecycle.setText(parameter, "text") == ParameterNotInitialized, "a setter accepted an uninitialized parameter"); Require(lifecycle.terminate(parameter) == ParameterTerminatedUninitialized, "terminate accepted an uninitialized parameter"); Require(initialize(parameter, nullptr, true) == ParameterInvalidArgument, "initialize accepted a null context"); Require(initialize(parameter, &context, true) == ParameterNotInitialized, "initialize accepted an uninitialized context"); Require(_ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE(nullptr, &context) == 0, "the context did not initialize"); Require(initialize(parameter, &context, false) == ParameterInvalidArgument, "initialize accepted a null argument"); - Require(lifecycle.setText(parameter, "text") == ParameterNotInitialized, "a failed initialize left the parameter initialized"); + Require(lifecycle.terminate(parameter) == ParameterTerminatedUninitialized, "a failed initialize left the parameter initialized"); Require(initialize(parameter, &context, true) == 0, "initialize failed"); Require(initialize(parameter, &context, true) == ParameterAlreadyInitialized, "initialize ran twice"); - Require(lifecycle.setText(parameter, nullptr) == ParameterInvalidArgument, "a setter accepted a null string"); - Require(lifecycle.setText(parameter, "text") == 0, "a setter failed"); + Require(lifecycle.setText == nullptr || lifecycle.setText(parameter, nullptr) == ParameterInvalidArgument, "a setter accepted a null string"); + Require(lifecycle.setText == nullptr || lifecycle.setText(parameter, "text") == 0, "a setter failed"); Require(lifecycle.terminate(parameter) == 0, "terminate failed"); Require(lifecycle.terminate(parameter) == ParameterTerminatedUninitialized, "terminate ran twice"); Require(initialize(parameter, &context, true) == 0, "initialize after terminate failed"); @@ -94,6 +95,67 @@ static void TestParameterLifecycles() { }); } +static IntrusivePtr body{reinterpret_cast<void*>(0x1000), nullptr, nullptr}; +static IntrusivePtr missingBody{nullptr, nullptr, nullptr}; + +static void TestSessionManagerParameterLifecycles() { + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsC1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsD1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions9terminateEv, + nullptr, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + const int result = _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_29PostPlayerSessionsRequestBodyEEE(parameter, context, present ? &body : &missingBody); + if (present || result != ParameterBodyMissing) return result; + return ParameterInvalidArgument; + }); + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerC1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerD1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer9terminateEv, + nullptr, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + const auto initialize = _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_51PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEE; + if (present) return initialize(parameter, context, "session", &body); + Require(initialize(parameter, context, "session", &missingBody) == ParameterBodyMissing, "initialize accepted a missing request body"); + return initialize(parameter, context, nullptr, &body); + }); + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionC1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionD1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession9terminateEv, + nullptr, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + return _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession10initializeEPNS1_6Common10LibContextEPKcSA_(parameter, context, "session", present ? "me" : nullptr); + }); + + std::uint64_t context = 0; + alignas(16) unsigned char parameter[0x10]; + std::memset(parameter, 0x5a, sizeof(parameter)); + const auto initialize = _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKc; + const auto setFilter = _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations26setinvitationInvalidFilterEPKc; + Require(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsC1Ev(parameter) == 0, "the invitations parameter constructor failed"); + for (const auto byte : parameter) Require(byte == 0, "the invitations parameter constructor left bytes"); + Require(setFilter(parameter, "filter") == ParameterNotInitialized, "the filter setter accepted an uninitialized parameter"); + Require(initialize(parameter, nullptr, "me") == ParameterInvalidArgument, "the invitations initialize accepted a null context"); + Require(initialize(parameter, &context, "me") == ParameterNotInitialized, "the invitations initialize accepted an uninitialized context"); + Require(_ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE(nullptr, &context) == 0, "the context did not initialize"); + Require(initialize(parameter, &context, nullptr) == ParameterInvalidArgument, "the invitations initialize accepted a null account"); + Require(setFilter(parameter, "filter") == ParameterNotInitialized, "a failed invitations initialize left the parameter initialized"); + Require(initialize(parameter, &context, "me") == 0, "the invitations initialize failed"); + Require(initialize(parameter, &context, "me") == ParameterAlreadyInitialized, "the invitations initialize ran twice"); + Require(setFilter(parameter, nullptr) == ParameterInvalidArgument, "the filter setter accepted a null string"); + Require(setFilter(parameter, "filter") == 0, "the filter setter failed"); + Require(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsD1Ev(parameter) == 0, "the invitations parameter destructor failed"); + Require(_ZN3sce2Np9CppWebApi6Common9terminateERNS2_10LibContextE(&context) == 0, "the context did not terminate"); +} + static void TestIteratorDestructors() { unsigned char iterator[0x20]; std::memset(iterator, 0x5a, sizeof(iterator)); @@ -154,6 +216,7 @@ static void TestVectorSizeAndElements() { int main() { TestParameterLifecycles(); + TestSessionManagerParameterLifecycles(); TestIteratorDestructors(); TestVectorSizeAndElements(); std::puts("NpCppWebApi offline tests passed"); diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index f471b02d0b..27a420a14f 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -4,7 +4,7 @@ - [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `DownStreamTransaction<...>` (2 instantiations) and `Vector<IntrusivePtr<Leaderboards::V1::User>>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used. The constructors and destructors of the same five `Transaction<...>` instantiations succeed without keeping state - [sce::Np::CppWebApi::Leaderboards::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - request-body and parameter bookkeeping succeeds without storing state: `GetRankingRequestBody::set{StartSerialRank,Group,Limit,Users,Offset}`, `RecordScoreRequestBody::set{Comment,SmallData,NeedsTmpRank,ComparedDateTime,PcId}`, `User::{setAccountId,setPcId}`, `BoardsApi::ParameterToGetBoardDefinition::{initialize,terminate}`, `RecordApi::ParameterToRecordLargeData::{initialize,terminate}`, `RecordApi::ParameterToRecordScore::{initialize,setxPsnAtomicOperation,terminate}`, `ViewApi::ParameterToGetLargeDataByObjectId::{initialize,terminate}`, `ViewApi::ParameterToGetRanking::{initialize,setgetRankingRequestBody,terminate}` -- [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `PlayerSessionsApi::ParameterToCreatePlayerSessions::{ctor,dtor,initialize}`, `PlayerSessionsApi::ParameterToGetPlayerSessionInvitations::{ctor,dtor,initialize,setfields,setinvitationInvalidFilter}`, `PlayerSessionsApi::ParameterToJoinPlayerSessionAsPlayer::{ctor,dtor,initialize,terminate}`, `PlayerSessionsApi::ParameterToLeavePlayerSession::{ctor,dtor,initialize}`, `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}`, `RequestPlayerSessionPlayer::setPushContexts` +- [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}` and `RequestPlayerSessionPlayer::setPushContexts` succeed without storing state. The `PlayerSessionsApi` parameters `ParameterToCreatePlayerSessions`, `ParameterToGetPlayerSessionInvitations`, `ParameterToJoinPlayerSessionAsPlayer` and `ParameterToLeavePlayerSession` keep only whether they are initialized, as the TitleCloudStorage ones do: `initialize`, `terminate` and `setinvitationInvalidFilter` return the codes of the module bundled with PPSA21564, plus 0x80553512 when the request body of `ParameterToCreatePlayerSessions` or `ParameterToJoinPlayerSessionAsPlayer` is empty. They store none of the strings or bodies, and `setfields` stores nothing either: only a request would read them - [sce::Np::CppWebApi::TitleCloudStorage::V1 and InGameCatalog::V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData`, `DataApi::ParameterToGetMultiDataStatusesByUser` and `ContainerApi::ParameterToGetContainer` keep only whether they are initialized. `initialize`, `terminate` and the string setters return the codes of the module bundled with PPSA21564: 0x80553503 for a null `LibContext` or string, 0x80553502 for a `LibContext` that `Common::initialize` did not set or a parameter not yet initialized, 0x8055350f for a second `initialize` and 0x80553510 for `terminate` without one. They store none of the values, and `ParameterToGetContainer::set{sort,offset,limit,serviceLabel}` store nothing either: only a request would read them - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `FriendsApi::ParameterToGetFriends`, `PresenceApi::ParameterToGetBasicPresences`, `BlocksApi::ParameterToGetBlockingUsers`, `ProfanityApi::ParameterToFilterProfanity` and `CommunicationRestrictionStatusApi::ParameterToGetCommunicationRestrictionStatus`, each `{ctor,dtor,initialize,terminate}` From dd5121f76e4f60ef370c4e3ee20d2ef4b796bcae Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 22:04:24 +1300 Subject: [PATCH 420/702] fix(libSceAgcDriver): drop the comment from the spare context register storage --- core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index a37ae3a4d1..44e6d64646 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -260,8 +260,6 @@ inline Registers InitialContextRegisters() { return InitialContextRegisterSet(); } -// Storage a pushed context state returns to when it is popped, so the next push copies into -// buffers it already has. A copied queue state starts without one. struct SpareRegisters { Registers registers; SpareRegisters() = default; From 52ec25aaaecf925739f9e39f8dae0187718d54c5 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:17 +0000 Subject: [PATCH 421/702] feat(libSceFreeType): implement FT_Stroker_New --- core/libs/prx/libSceFreeType/CMakeLists.txt | 1 + core/libs/prx/libSceFreeType/src/Stroker.cpp | 51 ++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/src/Stroker.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 9682b1e0b3..7e08fa85ac 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -4,6 +4,7 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL src/Outline.cpp src/Charmap.cpp src/FaceInfo.cpp + src/Stroker.cpp src/GuestObjects.cpp src/Names.cpp src/Trigonometry.cpp diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp new file mode 100644 index 0000000000..6778a17e46 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -0,0 +1,51 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_STROKER_H + +#include <cstdint> +#include <limits> +#include <stdexcept> +#include <string> + +using namespace FreeType; + +namespace { + +FT_Pos HostCoordinate(std::int64_t value, const char* function) { + if (value < std::numeric_limits<FT_Pos>::min() || value > std::numeric_limits<FT_Pos>::max()) { + throw std::runtime_error(std::string(function) + ": a value outside the host FT_Pos range is not implemented"); + } + return static_cast<FT_Pos>(value); +} + +struct NativeVector { + FT_Vector value{}; + FT_Vector* pointer = nullptr; + + NativeVector(const GuestVector* guest, const char* function) { + if (!guest) return; + value = {HostCoordinate(guest->x, function), HostCoordinate(guest->y, function)}; + pointer = &value; + } +}; + +void WriteCount(std::uint32_t* target, FT_UInt value) { + if (!target) return; + GuestArena::HostWrite write(target, sizeof(*target)); + *target = value; +} + +} + +extern "C" { + +int APS5_VABI FT_Stroker_New_nid_postfix(GuestLibrary* library, void** stroker) { + FT_Stroker native = nullptr; + const FT_Error error = FT_Stroker_New(library ? library->host : nullptr, stroker ? &native : nullptr); + if (stroker) *stroker = native; + return error; +} + +} From ff203b54440e344aed07be8a7c99b8c80da36f14 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:23 +0000 Subject: [PATCH 422/702] test(libSceFreeType): compare FT_Stroker_New with native FreeType --- core/libs/prx/libSceFreeType/CMakeLists.txt | 5 +++ core/libs/tests/GuestFreeTypeStroker.cpp | 42 +++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 core/libs/tests/GuestFreeTypeStroker.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 7e08fa85ac..fa8e741702 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -89,6 +89,11 @@ if(BUILD_TESTING) set_tests_properties(guest_freetype_bitmap_init PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFreeType>") endif() + add_test_executable(guest_freetype_stroker_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeStroker.cpp) + target_include_directories(guest_freetype_stroker_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_stroker_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_stroker_tests) + add_test(NAME guest_freetype_stroker COMMAND guest_freetype_stroker_tests) add_test_executable(guest_freetype_charmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmap.cpp) target_include_directories(guest_freetype_charmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp new file mode 100644 index 0000000000..11aa5c6e40 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -0,0 +1,42 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_STROKER_H + +#include <cstdint> +#include <cstdio> +#include <cstdlib> + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Stroker_New_nid_postfix(GuestLibrary*, void**); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType stroker check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void TestNew(GuestLibrary& library, FT_Library native) { + void* guest = reinterpret_cast<void*>(1); + FT_Stroker expected = nullptr; + Require(FT_Stroker_New_nid_postfix(nullptr, &guest) == FT_Stroker_New(nullptr, &expected)); + Require(guest == nullptr && expected == nullptr); + Require(FT_Stroker_New_nid_postfix(&library, nullptr) == FT_Stroker_New(native, nullptr)); + Require(FT_Stroker_New_nid_postfix(&library, &guest) == FT_Stroker_New(native, &expected)); + Require(guest != nullptr && expected != nullptr); + FT_Stroker_Done(static_cast<FT_Stroker>(guest)); + FT_Stroker_Done(expected); +} + +int main() { + GuestLibrary library{}; + FT_Library native = nullptr; + Require(FT_Init_FreeType(&library.host) == 0 && FT_Init_FreeType(&native) == 0); + TestNew(library, native); + Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); +} From c266d247d0a8a71e2835f4a9cca42c0906458497 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:23 +0000 Subject: [PATCH 423/702] docs(libSceFreeType): record FT_Stroker_New --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6542a65909..be76de2900 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -5,6 +5,7 @@ - [FT_Reference_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signature, reference-counter limits and finalizer behavior are unverified; the LP64 guest adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Counter overflow and reentrant finalization have not been measured on the console. +- [FT_Stroker_New](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The returned stroker is a native object allocated through the guest library; the guest treats it as an opaque handle - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From 6ec8ee804c6d16f4072738810be517dddc0bf870 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:23 +0000 Subject: [PATCH 424/702] feat(libSceFreeType): implement FT_Stroker_Done --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index 6778a17e46..dcb3000e09 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -48,4 +48,8 @@ int APS5_VABI FT_Stroker_New_nid_postfix(GuestLibrary* library, void** stroker) return error; } +void APS5_VABI FT_Stroker_Done_nid_postfix(void* stroker) { + FT_Stroker_Done(static_cast<FT_Stroker>(stroker)); +} + } From 47c79888e7ef835cb88fde6ce9da40b8b0255421 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:28 +0000 Subject: [PATCH 425/702] test(libSceFreeType): compare FT_Stroker_Done with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 11aa5c6e40..4d1fab9094 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -11,6 +11,7 @@ using namespace FreeType; extern "C" { int APS5_VABI FT_Stroker_New_nid_postfix(GuestLibrary*, void**); +void APS5_VABI FT_Stroker_Done_nid_postfix(void*); } static void Check(bool value, int line) { @@ -33,10 +34,23 @@ static void TestNew(GuestLibrary& library, FT_Library native) { FT_Stroker_Done(expected); } +static void TestDone(GuestLibrary& library, FT_Library native) { + for (int round = 0; round < 3; ++round) { + void* guest = nullptr; + FT_Stroker expected = nullptr; + Require(FT_Stroker_New_nid_postfix(&library, &guest) == 0 && FT_Stroker_New(native, &expected) == 0); + FT_Stroker_Done_nid_postfix(guest); + FT_Stroker_Done(expected); + } + FT_Stroker_Done_nid_postfix(nullptr); + FT_Stroker_Done(nullptr); +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; Require(FT_Init_FreeType(&library.host) == 0 && FT_Init_FreeType(&native) == 0); TestNew(library, native); + TestDone(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From 959e8154726fb61d40739649c303fd5e5605a6a4 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:28 +0000 Subject: [PATCH 426/702] docs(libSceFreeType): record FT_Stroker_Done --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index be76de2900..69eba9569a 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -6,6 +6,7 @@ - [FT_Reference_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signature, reference-counter limits and finalizer behavior are unverified; the LP64 guest adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Counter overflow and reentrant finalization have not been measured on the console. - [FT_Stroker_New](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The returned stroker is a native object allocated through the guest library; the guest treats it as an opaque handle +- [FT_Stroker_Done](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The handle must come from `FT_Stroker_New` and is released by the native library - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From 775b4d68123eef63a5b992b9093fc98357ae01aa Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:28 +0000 Subject: [PATCH 427/702] feat(libSceFreeType): implement FT_Stroker_GetCounts --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index dcb3000e09..3374ba4e38 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -52,4 +52,13 @@ void APS5_VABI FT_Stroker_Done_nid_postfix(void* stroker) { FT_Stroker_Done(static_cast<FT_Stroker>(stroker)); } +int APS5_VABI FT_Stroker_GetCounts_nid_postfix(void* stroker, std::uint32_t* points, std::uint32_t* contours) { + FT_UInt nativePoints = 0; + FT_UInt nativeContours = 0; + const FT_Error error = FT_Stroker_GetCounts(static_cast<FT_Stroker>(stroker), points ? &nativePoints : nullptr, contours ? &nativeContours : nullptr); + WriteCount(points, nativePoints); + WriteCount(contours, nativeContours); + return error; +} + } From d0fc1eb62c5ee168b088e08033f47d025c8867bb Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:34 +0000 Subject: [PATCH 428/702] test(libSceFreeType): compare FT_Stroker_GetCounts with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 4d1fab9094..93c7869d5f 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -12,6 +12,7 @@ using namespace FreeType; extern "C" { int APS5_VABI FT_Stroker_New_nid_postfix(GuestLibrary*, void**); void APS5_VABI FT_Stroker_Done_nid_postfix(void*); +int APS5_VABI FT_Stroker_GetCounts_nid_postfix(void*, std::uint32_t*, std::uint32_t*); } static void Check(bool value, int line) { @@ -22,6 +23,32 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +struct Pair { + void* guest = nullptr; + FT_Stroker native = nullptr; + + Pair(GuestLibrary& library, FT_Library nativeLibrary) { + Require(FT_Stroker_New_nid_postfix(&library, &guest) == 0); + Require(FT_Stroker_New(nativeLibrary, &native) == 0); + FT_Stroker_Set(static_cast<FT_Stroker>(guest), 64, FT_STROKER_LINECAP_BUTT, FT_STROKER_LINEJOIN_ROUND, 0); + FT_Stroker_Set(native, 64, FT_STROKER_LINECAP_BUTT, FT_STROKER_LINEJOIN_ROUND, 0); + } + + ~Pair() { + FT_Stroker_Done_nid_postfix(guest); + FT_Stroker_Done(native); + } + + void CountsMatch() const { + std::uint32_t points = 11; + std::uint32_t contours = 13; + FT_UInt nativePoints = 11; + FT_UInt nativeContours = 13; + Require(FT_Stroker_GetCounts_nid_postfix(guest, &points, &contours) == FT_Stroker_GetCounts(native, &nativePoints, &nativeContours)); + Require(points == nativePoints && contours == nativeContours); + } +}; + static void TestNew(GuestLibrary& library, FT_Library native) { void* guest = reinterpret_cast<void*>(1); FT_Stroker expected = nullptr; @@ -46,11 +73,24 @@ static void TestDone(GuestLibrary& library, FT_Library native) { FT_Stroker_Done(nullptr); } +static void TestGetCounts(GuestLibrary& library, FT_Library native) { + Pair pair(library, native); + pair.CountsMatch(); + std::uint32_t points = 7; + std::uint32_t contours = 9; + FT_UInt nativePoints = 7; + FT_UInt nativeContours = 9; + Require(FT_Stroker_GetCounts_nid_postfix(nullptr, &points, &contours) == FT_Stroker_GetCounts(nullptr, &nativePoints, &nativeContours)); + Require(points == nativePoints && contours == nativeContours); + Require(FT_Stroker_GetCounts_nid_postfix(pair.guest, nullptr, nullptr) == FT_Stroker_GetCounts(pair.native, nullptr, nullptr)); +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; Require(FT_Init_FreeType(&library.host) == 0 && FT_Init_FreeType(&native) == 0); TestNew(library, native); TestDone(library, native); + TestGetCounts(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From b030f8b5af0a3b6a92836187429c3f9d22c9d4ba Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:34 +0000 Subject: [PATCH 429/702] docs(libSceFreeType): record FT_Stroker_GetCounts --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 69eba9569a..ede37447bd 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -7,6 +7,7 @@ - [FT_Stroker_New](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The returned stroker is a native object allocated through the guest library; the guest treats it as an opaque handle - [FT_Stroker_Done](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The handle must come from `FT_Stroker_New` and is released by the native library +- [FT_Stroker_GetCounts](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). Counts are written only to non-null guest pointers - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From 192f32d941f8176edab0928e209a14df1746f23f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:34 +0000 Subject: [PATCH 430/702] feat(libSceFreeType): implement FT_Stroker_BeginSubPath --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index 3374ba4e38..d221d7e8e4 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -61,4 +61,9 @@ int APS5_VABI FT_Stroker_GetCounts_nid_postfix(void* stroker, std::uint32_t* poi return error; } +int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void* stroker, const GuestVector* to, std::uint8_t open) { + NativeVector vector(to, __func__); + return FT_Stroker_BeginSubPath(static_cast<FT_Stroker>(stroker), vector.pointer, open); +} + } From 89cfa496c8e0b7fa260aca0d228f4c9cead4184c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:39 +0000 Subject: [PATCH 431/702] test(libSceFreeType): compare FT_Stroker_BeginSubPath with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 93c7869d5f..4794be40f7 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -13,6 +13,7 @@ extern "C" { int APS5_VABI FT_Stroker_New_nid_postfix(GuestLibrary*, void**); void APS5_VABI FT_Stroker_Done_nid_postfix(void*); int APS5_VABI FT_Stroker_GetCounts_nid_postfix(void*, std::uint32_t*, std::uint32_t*); +int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void*, const GuestVector*, std::uint8_t); } static void Check(bool value, int line) { @@ -85,6 +86,21 @@ static void TestGetCounts(GuestLibrary& library, FT_Library native) { Require(FT_Stroker_GetCounts_nid_postfix(pair.guest, nullptr, nullptr) == FT_Stroker_GetCounts(pair.native, nullptr, nullptr)); } +static void TestBeginSubPath(GuestLibrary& library, FT_Library native) { + for (const std::uint8_t open : {0, 1}) { + Pair pair(library, native); + const GuestVector start{64, 128}; + FT_Vector nativeStart{64, 128}; + Require(FT_Stroker_BeginSubPath_nid_postfix(pair.guest, &start, open) == FT_Stroker_BeginSubPath(pair.native, &nativeStart, open)); + pair.CountsMatch(); + } + Pair pair(library, native); + const GuestVector start{0, 0}; + FT_Vector nativeStart{0, 0}; + Require(FT_Stroker_BeginSubPath_nid_postfix(nullptr, &start, 0) == FT_Stroker_BeginSubPath(nullptr, &nativeStart, 0)); + Require(FT_Stroker_BeginSubPath_nid_postfix(pair.guest, nullptr, 0) == FT_Stroker_BeginSubPath(pair.native, nullptr, 0)); +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; @@ -92,5 +108,6 @@ int main() { TestNew(library, native); TestDone(library, native); TestGetCounts(library, native); + TestBeginSubPath(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From a13015d72e3f50a73454e39af72c0535a4d59838 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:39 +0000 Subject: [PATCH 432/702] docs(libSceFreeType): record FT_Stroker_BeginSubPath --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index ede37447bd..b84d20252c 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -8,6 +8,7 @@ - [FT_Stroker_New](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The returned stroker is a native object allocated through the guest library; the guest treats it as an opaque handle - [FT_Stroker_Done](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The handle must come from `FT_Stroker_New` and is released by the native library - [FT_Stroker_GetCounts](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). Counts are written only to non-null guest pointers +- [FT_Stroker_BeginSubPath](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From 44907b28bef92de3e0023464bae0a71674c850ab Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:44 +0000 Subject: [PATCH 433/702] feat(libSceFreeType): implement FT_Stroker_LineTo --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index d221d7e8e4..c4f8a64546 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -66,4 +66,9 @@ int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void* stroker, const GuestVect return FT_Stroker_BeginSubPath(static_cast<FT_Stroker>(stroker), vector.pointer, open); } +int APS5_VABI FT_Stroker_LineTo_nid_postfix(void* stroker, const GuestVector* to) { + NativeVector vector(to, __func__); + return FT_Stroker_LineTo(static_cast<FT_Stroker>(stroker), vector.pointer); +} + } From 8d41c9d664ad50fdb5b9f97c692ba03445039401 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:50 +0000 Subject: [PATCH 434/702] test(libSceFreeType): compare FT_Stroker_LineTo with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 4794be40f7..852b7d65df 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -14,6 +14,7 @@ int APS5_VABI FT_Stroker_New_nid_postfix(GuestLibrary*, void**); void APS5_VABI FT_Stroker_Done_nid_postfix(void*); int APS5_VABI FT_Stroker_GetCounts_nid_postfix(void*, std::uint32_t*, std::uint32_t*); int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void*, const GuestVector*, std::uint8_t); +int APS5_VABI FT_Stroker_LineTo_nid_postfix(void*, const GuestVector*); } static void Check(bool value, int line) { @@ -101,6 +102,22 @@ static void TestBeginSubPath(GuestLibrary& library, FT_Library native) { Require(FT_Stroker_BeginSubPath_nid_postfix(pair.guest, nullptr, 0) == FT_Stroker_BeginSubPath(pair.native, nullptr, 0)); } +static void TestLineTo(GuestLibrary& library, FT_Library native) { + Pair pair(library, native); + const GuestVector start{0, 0}; + FT_Vector nativeStart{0, 0}; + Require(FT_Stroker_BeginSubPath_nid_postfix(pair.guest, &start, 1) == FT_Stroker_BeginSubPath(pair.native, &nativeStart, 1)); + for (const GuestVector point : {GuestVector{640, 0}, GuestVector{640, 640}, GuestVector{0, 640}}) { + FT_Vector nativePoint{static_cast<FT_Pos>(point.x), static_cast<FT_Pos>(point.y)}; + Require(FT_Stroker_LineTo_nid_postfix(pair.guest, &point) == FT_Stroker_LineTo(pair.native, &nativePoint)); + pair.CountsMatch(); + } + const GuestVector point{1, 1}; + FT_Vector nativePoint{1, 1}; + Require(FT_Stroker_LineTo_nid_postfix(nullptr, &point) == FT_Stroker_LineTo(nullptr, &nativePoint)); + Require(FT_Stroker_LineTo_nid_postfix(pair.guest, nullptr) == FT_Stroker_LineTo(pair.native, nullptr)); +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; @@ -109,5 +126,6 @@ int main() { TestDone(library, native); TestGetCounts(library, native); TestBeginSubPath(library, native); + TestLineTo(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From b3e919312c1adfbe157a208e4930e7e8afcc9d1a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:50 +0000 Subject: [PATCH 435/702] docs(libSceFreeType): record FT_Stroker_LineTo --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index b84d20252c..fe1cf50174 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -9,6 +9,7 @@ - [FT_Stroker_Done](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The handle must come from `FT_Stroker_New` and is released by the native library - [FT_Stroker_GetCounts](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). Counts are written only to non-null guest pointers - [FT_Stroker_BeginSubPath](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits +- [FT_Stroker_LineTo](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From d76e9a89d94ff33a8d8547e9bc49cd61d0b3cbd8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:50 +0000 Subject: [PATCH 436/702] feat(libSceFreeType): implement FT_Stroker_EndSubPath --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index c4f8a64546..ae0dd9230d 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -71,4 +71,8 @@ int APS5_VABI FT_Stroker_LineTo_nid_postfix(void* stroker, const GuestVector* to return FT_Stroker_LineTo(static_cast<FT_Stroker>(stroker), vector.pointer); } +int APS5_VABI FT_Stroker_EndSubPath_nid_postfix(void* stroker) { + return FT_Stroker_EndSubPath(static_cast<FT_Stroker>(stroker)); +} + } From 4e99b7a18d63b02dce227d36d24b6dd3a841aa7a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:55 +0000 Subject: [PATCH 437/702] test(libSceFreeType): compare FT_Stroker_EndSubPath with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 852b7d65df..25b8121d91 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -15,6 +15,7 @@ void APS5_VABI FT_Stroker_Done_nid_postfix(void*); int APS5_VABI FT_Stroker_GetCounts_nid_postfix(void*, std::uint32_t*, std::uint32_t*); int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void*, const GuestVector*, std::uint8_t); int APS5_VABI FT_Stroker_LineTo_nid_postfix(void*, const GuestVector*); +int APS5_VABI FT_Stroker_EndSubPath_nid_postfix(void*); } static void Check(bool value, int line) { @@ -49,6 +50,18 @@ struct Pair { Require(FT_Stroker_GetCounts_nid_postfix(guest, &points, &contours) == FT_Stroker_GetCounts(native, &nativePoints, &nativeContours)); Require(points == nativePoints && contours == nativeContours); } + + void Square(std::uint8_t open, bool finish = true) const { + const GuestVector corners[] = {{0, 0}, {640, 0}, {640, 640}, {0, 640}}; + FT_Vector nativeStart{0, 0}; + Require(FT_Stroker_BeginSubPath_nid_postfix(guest, &corners[0], open) == FT_Stroker_BeginSubPath(native, &nativeStart, open)); + for (int index = 1; index < 4; ++index) { + FT_Vector nativePoint{static_cast<FT_Pos>(corners[index].x), static_cast<FT_Pos>(corners[index].y)}; + Require(FT_Stroker_LineTo_nid_postfix(guest, &corners[index]) == FT_Stroker_LineTo(native, &nativePoint)); + } + if (!finish) return; + Require(FT_Stroker_EndSubPath_nid_postfix(guest) == FT_Stroker_EndSubPath(native)); + } }; static void TestNew(GuestLibrary& library, FT_Library native) { @@ -118,6 +131,16 @@ static void TestLineTo(GuestLibrary& library, FT_Library native) { Require(FT_Stroker_LineTo_nid_postfix(pair.guest, nullptr) == FT_Stroker_LineTo(pair.native, nullptr)); } +static void TestEndSubPath(GuestLibrary& library, FT_Library native) { + for (const std::uint8_t open : {0, 1}) { + Pair pair(library, native); + pair.Square(open, false); + Require(FT_Stroker_EndSubPath_nid_postfix(pair.guest) == FT_Stroker_EndSubPath(pair.native)); + pair.CountsMatch(); + } + Require(FT_Stroker_EndSubPath_nid_postfix(nullptr) == FT_Stroker_EndSubPath(nullptr)); +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; @@ -127,5 +150,6 @@ int main() { TestGetCounts(library, native); TestBeginSubPath(library, native); TestLineTo(library, native); + TestEndSubPath(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From 560f9651c08fe3e978008ff2d1f1176fc74d1a29 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:55 +0000 Subject: [PATCH 438/702] docs(libSceFreeType): record FT_Stroker_EndSubPath --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index fe1cf50174..5412205d92 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -10,6 +10,7 @@ - [FT_Stroker_GetCounts](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). Counts are written only to non-null guest pointers - [FT_Stroker_BeginSubPath](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Stroker_LineTo](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits +- [FT_Stroker_EndSubPath](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From 1121e7d97f14189357f0a6d65a3b3e5219074081 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:55:55 +0000 Subject: [PATCH 439/702] feat(libSceFreeType): implement FT_Stroker_Set --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index ae0dd9230d..ba581ee768 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -75,4 +75,9 @@ int APS5_VABI FT_Stroker_EndSubPath_nid_postfix(void* stroker) { return FT_Stroker_EndSubPath(static_cast<FT_Stroker>(stroker)); } +void APS5_VABI FT_Stroker_Set_nid_postfix(void* stroker, std::int64_t radius, std::int32_t lineCap, std::int32_t lineJoin, std::int64_t miterLimit) { + FT_Stroker_Set(static_cast<FT_Stroker>(stroker), HostCoordinate(radius, __func__), static_cast<FT_Stroker_LineCap>(lineCap), + static_cast<FT_Stroker_LineJoin>(lineJoin), HostCoordinate(miterLimit, __func__)); +} + } From 7534ee64c2526615918214ec052f1b3148ed41b6 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:00 +0000 Subject: [PATCH 440/702] test(libSceFreeType): compare FT_Stroker_Set with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 25b8121d91..844865c31e 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -16,6 +16,7 @@ int APS5_VABI FT_Stroker_GetCounts_nid_postfix(void*, std::uint32_t*, std::uint3 int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void*, const GuestVector*, std::uint8_t); int APS5_VABI FT_Stroker_LineTo_nid_postfix(void*, const GuestVector*); int APS5_VABI FT_Stroker_EndSubPath_nid_postfix(void*); +void APS5_VABI FT_Stroker_Set_nid_postfix(void*, std::int64_t, std::int32_t, std::int32_t, std::int64_t); } static void Check(bool value, int line) { @@ -141,6 +142,21 @@ static void TestEndSubPath(GuestLibrary& library, FT_Library native) { Require(FT_Stroker_EndSubPath_nid_postfix(nullptr) == FT_Stroker_EndSubPath(nullptr)); } +static void TestSet(GuestLibrary& library, FT_Library native) { + for (const std::int64_t radius : {0, 16, 64, 200}) { + for (const std::int32_t cap : {0, 1, 2}) { + for (const std::int32_t join : {0, 1, 2, 3}) { + Pair pair(library, native); + FT_Stroker_Set_nid_postfix(pair.guest, radius, cap, join, 0x20000); + FT_Stroker_Set(pair.native, static_cast<FT_Fixed>(radius), static_cast<FT_Stroker_LineCap>(cap), static_cast<FT_Stroker_LineJoin>(join), 0x20000); + pair.Square(0); + pair.Square(1); + pair.CountsMatch(); + } + } + } +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; @@ -151,5 +167,6 @@ int main() { TestBeginSubPath(library, native); TestLineTo(library, native); TestEndSubPath(library, native); + TestSet(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From da4ad465704a826681d63c5aa9f0519bd52b5f79 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:01 +0000 Subject: [PATCH 441/702] docs(libSceFreeType): record FT_Stroker_Set --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 5412205d92..30d1fd1726 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -11,6 +11,7 @@ - [FT_Stroker_BeginSubPath](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Stroker_LineTo](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Stroker_EndSubPath](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits +- [FT_Stroker_Set](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A radius or miter limit outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From dc19e9adc8bbddf3504696ab3724d53c59f5f8a8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:01 +0000 Subject: [PATCH 442/702] feat(libSceFreeType): implement FT_Stroker_Rewind --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index ba581ee768..505b5426e2 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -80,4 +80,8 @@ void APS5_VABI FT_Stroker_Set_nid_postfix(void* stroker, std::int64_t radius, st static_cast<FT_Stroker_LineJoin>(lineJoin), HostCoordinate(miterLimit, __func__)); } +void APS5_VABI FT_Stroker_Rewind_nid_postfix(void* stroker) { + FT_Stroker_Rewind(static_cast<FT_Stroker>(stroker)); +} + } From 4561ddf6e9fab35fd8f171097487da98e050536b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:06 +0000 Subject: [PATCH 443/702] test(libSceFreeType): compare FT_Stroker_Rewind with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 844865c31e..734c38e5c4 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -17,6 +17,7 @@ int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void*, const GuestVector*, std int APS5_VABI FT_Stroker_LineTo_nid_postfix(void*, const GuestVector*); int APS5_VABI FT_Stroker_EndSubPath_nid_postfix(void*); void APS5_VABI FT_Stroker_Set_nid_postfix(void*, std::int64_t, std::int32_t, std::int32_t, std::int64_t); +void APS5_VABI FT_Stroker_Rewind_nid_postfix(void*); } static void Check(bool value, int line) { @@ -157,6 +158,22 @@ static void TestSet(GuestLibrary& library, FT_Library native) { } } +static void TestRewind(GuestLibrary& library, FT_Library native) { + Pair pair(library, native); + FT_Stroker_Set_nid_postfix(pair.guest, 64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + FT_Stroker_Set(pair.native, 64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + pair.Square(0); + pair.CountsMatch(); + std::uint32_t points = 0; + std::uint32_t contours = 0; + Require(FT_Stroker_GetCounts_nid_postfix(pair.guest, &points, &contours) == 0 && points != 0 && contours != 0); + FT_Stroker_Rewind_nid_postfix(pair.guest); + FT_Stroker_Rewind(pair.native); + Require(FT_Stroker_GetCounts_nid_postfix(pair.guest, &points, &contours) == 0 && points == 0 && contours == 0); + pair.CountsMatch(); + FT_Stroker_Rewind_nid_postfix(nullptr); +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; @@ -168,5 +185,6 @@ int main() { TestLineTo(library, native); TestEndSubPath(library, native); TestSet(library, native); + TestRewind(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From 8e66aee1ddc9293228bdcc9e25d67a3eb4976b8f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:06 +0000 Subject: [PATCH 444/702] docs(libSceFreeType): record FT_Stroker_Rewind --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 30d1fd1726..fab65b6020 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -12,6 +12,7 @@ - [FT_Stroker_LineTo](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Stroker_EndSubPath](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Stroker_Set](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A radius or miter limit outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits +- [FT_Stroker_Rewind](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The call keeps the radius, cap and join set by `FT_Stroker_Set` - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From 48437be1e2dd0644657d03481249addc0788e4e3 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:12 +0000 Subject: [PATCH 445/702] feat(libSceFreeType): implement FT_Stroker_GetBorderCounts --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index 505b5426e2..901b60ab64 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -84,4 +84,14 @@ void APS5_VABI FT_Stroker_Rewind_nid_postfix(void* stroker) { FT_Stroker_Rewind(static_cast<FT_Stroker>(stroker)); } +int APS5_VABI FT_Stroker_GetBorderCounts_nid_postfix(void* stroker, std::int32_t border, std::uint32_t* points, std::uint32_t* contours) { + FT_UInt nativePoints = 0; + FT_UInt nativeContours = 0; + const FT_Error error = FT_Stroker_GetBorderCounts(static_cast<FT_Stroker>(stroker), static_cast<FT_StrokerBorder>(border), + points ? &nativePoints : nullptr, contours ? &nativeContours : nullptr); + WriteCount(points, nativePoints); + WriteCount(contours, nativeContours); + return error; +} + } From d25286fc7c3c9ea0cba43a6862c994577fc39f31 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:18 +0000 Subject: [PATCH 446/702] test(libSceFreeType): compare FT_Stroker_GetBorderCounts with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 734c38e5c4..c538e9ba8d 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -18,6 +18,7 @@ int APS5_VABI FT_Stroker_LineTo_nid_postfix(void*, const GuestVector*); int APS5_VABI FT_Stroker_EndSubPath_nid_postfix(void*); void APS5_VABI FT_Stroker_Set_nid_postfix(void*, std::int64_t, std::int32_t, std::int32_t, std::int64_t); void APS5_VABI FT_Stroker_Rewind_nid_postfix(void*); +int APS5_VABI FT_Stroker_GetBorderCounts_nid_postfix(void*, std::int32_t, std::uint32_t*, std::uint32_t*); } static void Check(bool value, int line) { @@ -174,6 +175,23 @@ static void TestRewind(GuestLibrary& library, FT_Library native) { FT_Stroker_Rewind_nid_postfix(nullptr); } +static void TestGetBorderCounts(GuestLibrary& library, FT_Library native) { + Pair pair(library, native); + FT_Stroker_Set_nid_postfix(pair.guest, 64, FT_STROKER_LINECAP_SQUARE, FT_STROKER_LINEJOIN_BEVEL, 0); + FT_Stroker_Set(pair.native, 64, FT_STROKER_LINECAP_SQUARE, FT_STROKER_LINEJOIN_BEVEL, 0); + pair.Square(0); + for (const std::int32_t border : {0, 1, 2, -1}) { + std::uint32_t points = 5; + std::uint32_t contours = 6; + FT_UInt nativePoints = 5; + FT_UInt nativeContours = 6; + Require(FT_Stroker_GetBorderCounts_nid_postfix(pair.guest, border, &points, &contours) == + FT_Stroker_GetBorderCounts(pair.native, static_cast<FT_StrokerBorder>(border), &nativePoints, &nativeContours)); + Require(points == nativePoints && contours == nativeContours); + } + Require(FT_Stroker_GetBorderCounts_nid_postfix(nullptr, 0, nullptr, nullptr) == FT_Stroker_GetBorderCounts(nullptr, FT_STROKER_BORDER_LEFT, nullptr, nullptr)); +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; @@ -186,5 +204,6 @@ int main() { TestEndSubPath(library, native); TestSet(library, native); TestRewind(library, native); + TestGetBorderCounts(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From 845c4280cc0a1611cd2394749216f52f79893788 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:18 +0000 Subject: [PATCH 447/702] docs(libSceFreeType): record FT_Stroker_GetBorderCounts --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index fab65b6020..e58e05d13f 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -13,6 +13,7 @@ - [FT_Stroker_EndSubPath](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Stroker_Set](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A radius or miter limit outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Stroker_Rewind](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The call keeps the radius, cap and join set by `FT_Stroker_Set` +- [FT_Stroker_GetBorderCounts](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). Counts are written only to non-null guest pointers - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From 8224bf1def7df84fa4e526127b4da1090c0ff317 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:18 +0000 Subject: [PATCH 448/702] feat(libSceFreeType): implement FT_Stroker_ConicTo --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index 901b60ab64..04d0d35cf3 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -94,4 +94,10 @@ int APS5_VABI FT_Stroker_GetBorderCounts_nid_postfix(void* stroker, std::int32_t return error; } +int APS5_VABI FT_Stroker_ConicTo_nid_postfix(void* stroker, const GuestVector* control, const GuestVector* to) { + NativeVector nativeControl(control, __func__); + NativeVector nativeTo(to, __func__); + return FT_Stroker_ConicTo(static_cast<FT_Stroker>(stroker), nativeControl.pointer, nativeTo.pointer); +} + } From 7aaf18a152731036e20626e28882d0e3eef6727f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:23 +0000 Subject: [PATCH 449/702] test(libSceFreeType): compare FT_Stroker_ConicTo with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index c538e9ba8d..6a0072d34d 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -19,6 +19,7 @@ int APS5_VABI FT_Stroker_EndSubPath_nid_postfix(void*); void APS5_VABI FT_Stroker_Set_nid_postfix(void*, std::int64_t, std::int32_t, std::int32_t, std::int64_t); void APS5_VABI FT_Stroker_Rewind_nid_postfix(void*); int APS5_VABI FT_Stroker_GetBorderCounts_nid_postfix(void*, std::int32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI FT_Stroker_ConicTo_nid_postfix(void*, const GuestVector*, const GuestVector*); } static void Check(bool value, int line) { @@ -192,6 +193,24 @@ static void TestGetBorderCounts(GuestLibrary& library, FT_Library native) { Require(FT_Stroker_GetBorderCounts_nid_postfix(nullptr, 0, nullptr, nullptr) == FT_Stroker_GetBorderCounts(nullptr, FT_STROKER_BORDER_LEFT, nullptr, nullptr)); } +static void TestConicTo(GuestLibrary& library, FT_Library native) { + Pair pair(library, native); + FT_Stroker_Set_nid_postfix(pair.guest, 48, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + FT_Stroker_Set(pair.native, 48, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + const GuestVector start{0, 0}; + FT_Vector nativeStart{0, 0}; + Require(FT_Stroker_BeginSubPath_nid_postfix(pair.guest, &start, 1) == FT_Stroker_BeginSubPath(pair.native, &nativeStart, 1)); + const GuestVector control{320, 640}; + const GuestVector to{640, 0}; + FT_Vector nativeControl{320, 640}; + FT_Vector nativeTo{640, 0}; + Require(FT_Stroker_ConicTo_nid_postfix(pair.guest, &control, &to) == FT_Stroker_ConicTo(pair.native, &nativeControl, &nativeTo)); + Require(FT_Stroker_EndSubPath_nid_postfix(pair.guest) == FT_Stroker_EndSubPath(pair.native)); + pair.CountsMatch(); + Require(FT_Stroker_ConicTo_nid_postfix(pair.guest, nullptr, &to) == FT_Stroker_ConicTo(pair.native, nullptr, &nativeTo)); + Require(FT_Stroker_ConicTo_nid_postfix(nullptr, &control, &to) == FT_Stroker_ConicTo(nullptr, &nativeControl, &nativeTo)); +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; @@ -205,5 +224,6 @@ int main() { TestSet(library, native); TestRewind(library, native); TestGetBorderCounts(library, native); + TestConicTo(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From 39a585461544c1d3cfe290504dd4c032a20fd58e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:23 +0000 Subject: [PATCH 450/702] docs(libSceFreeType): record FT_Stroker_ConicTo --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index e58e05d13f..78d1470d96 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -14,6 +14,7 @@ - [FT_Stroker_Set](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A radius or miter limit outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Stroker_Rewind](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The call keeps the radius, cap and join set by `FT_Stroker_Set` - [FT_Stroker_GetBorderCounts](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). Counts are written only to non-null guest pointers +- [FT_Stroker_ConicTo](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From b5dfb0be646086831cfbbacdfef2246fc719aab0 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:23 +0000 Subject: [PATCH 451/702] feat(libSceFreeType): implement FT_Stroker_CubicTo --- core/libs/prx/libSceFreeType/src/Stroker.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index 04d0d35cf3..c733f67a72 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -100,4 +100,11 @@ int APS5_VABI FT_Stroker_ConicTo_nid_postfix(void* stroker, const GuestVector* c return FT_Stroker_ConicTo(static_cast<FT_Stroker>(stroker), nativeControl.pointer, nativeTo.pointer); } +int APS5_VABI FT_Stroker_CubicTo_nid_postfix(void* stroker, const GuestVector* control1, const GuestVector* control2, const GuestVector* to) { + NativeVector first(control1, __func__); + NativeVector second(control2, __func__); + NativeVector end(to, __func__); + return FT_Stroker_CubicTo(static_cast<FT_Stroker>(stroker), first.pointer, second.pointer, end.pointer); +} + } From acfb431a8d5af52c107730bfd6f0a8020588ca97 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:29 +0000 Subject: [PATCH 452/702] test(libSceFreeType): compare FT_Stroker_CubicTo with native FreeType --- core/libs/tests/GuestFreeTypeStroker.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 6a0072d34d..2d86ca21a8 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -20,6 +20,7 @@ void APS5_VABI FT_Stroker_Set_nid_postfix(void*, std::int64_t, std::int32_t, std void APS5_VABI FT_Stroker_Rewind_nid_postfix(void*); int APS5_VABI FT_Stroker_GetBorderCounts_nid_postfix(void*, std::int32_t, std::uint32_t*, std::uint32_t*); int APS5_VABI FT_Stroker_ConicTo_nid_postfix(void*, const GuestVector*, const GuestVector*); +int APS5_VABI FT_Stroker_CubicTo_nid_postfix(void*, const GuestVector*, const GuestVector*, const GuestVector*); } static void Check(bool value, int line) { @@ -211,6 +212,26 @@ static void TestConicTo(GuestLibrary& library, FT_Library native) { Require(FT_Stroker_ConicTo_nid_postfix(nullptr, &control, &to) == FT_Stroker_ConicTo(nullptr, &nativeControl, &nativeTo)); } +static void TestCubicTo(GuestLibrary& library, FT_Library native) { + Pair pair(library, native); + FT_Stroker_Set_nid_postfix(pair.guest, 48, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + FT_Stroker_Set(pair.native, 48, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + const GuestVector start{0, 0}; + FT_Vector nativeStart{0, 0}; + Require(FT_Stroker_BeginSubPath_nid_postfix(pair.guest, &start, 1) == FT_Stroker_BeginSubPath(pair.native, &nativeStart, 1)); + const GuestVector first{0, 640}; + const GuestVector second{640, 640}; + const GuestVector to{640, 0}; + FT_Vector nativeFirst{0, 640}; + FT_Vector nativeSecond{640, 640}; + FT_Vector nativeTo{640, 0}; + Require(FT_Stroker_CubicTo_nid_postfix(pair.guest, &first, &second, &to) == FT_Stroker_CubicTo(pair.native, &nativeFirst, &nativeSecond, &nativeTo)); + Require(FT_Stroker_EndSubPath_nid_postfix(pair.guest) == FT_Stroker_EndSubPath(pair.native)); + pair.CountsMatch(); + Require(FT_Stroker_CubicTo_nid_postfix(pair.guest, &first, nullptr, &to) == FT_Stroker_CubicTo(pair.native, &nativeFirst, nullptr, &nativeTo)); + Require(FT_Stroker_CubicTo_nid_postfix(nullptr, &first, &second, &to) == FT_Stroker_CubicTo(nullptr, &nativeFirst, &nativeSecond, &nativeTo)); +} + int main() { GuestLibrary library{}; FT_Library native = nullptr; @@ -225,5 +246,6 @@ int main() { TestRewind(library, native); TestGetBorderCounts(library, native); TestConicTo(library, native); + TestCubicTo(library, native); Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(native) == 0); } From 9c2274e8968f60d4ab3107d130dd1c32f8f638e4 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:29 +0000 Subject: [PATCH 453/702] docs(libSceFreeType): record FT_Stroker_CubicTo --- docs/dev/debt/libSceFreeType.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 78d1470d96..1d42b18bd7 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -15,6 +15,7 @@ - [FT_Stroker_Rewind](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). The call keeps the radius, cap and join set by `FT_Stroker_Set` - [FT_Stroker_GetBorderCounts](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). Counts are written only to non-null guest pointers - [FT_Stroker_ConicTo](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits +- [FT_Stroker_CubicTo](../../../core/libs/prx/libSceFreeType/src/Stroker.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c). A guest coordinate outside the host `FT_Pos` range throws on Windows, where the native type is 32 bits - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive From cafa9574257931eec0965491a5383aa5b10d5ed3 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:13:20 +0000 Subject: [PATCH 454/702] test(freetype): set Windows DLL path for 10598 --- core/libs/prx/libSceFreeType/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index fa8e741702..32ea45c3c2 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -94,6 +94,10 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_stroker_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_stroker_tests) add_test(NAME guest_freetype_stroker COMMAND guest_freetype_stroker_tests) + if(WIN32) + set_tests_properties(guest_freetype_stroker PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFreeType>") + endif() add_test_executable(guest_freetype_charmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmap.cpp) target_include_directories(guest_freetype_charmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) From 49032fe9edbecccb1f81ad2abc3b721e56774d0e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:10:36 +0000 Subject: [PATCH 455/702] fix(libSceFreeTypeOt): name the aliases of the symbols this PR implements after them An Unknown export whose NID is a symbol of the tree is rejected by the unknown-nid convention check, so the alias takes the symbol's name, as the already identified ones do. --- core/libs/prx/libSceFreeTypeOt/Export.cpp | 44 +++++++++++------------ docs/dev/debt/libSceFreeTypeOt.md | 3 +- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index e17ac66491..ca6a6a1164 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -547,30 +547,30 @@ int APS5_VABI sceFreeTypeOtUnknown_Az4H9FlfxCA_nid_no_patch() { } APS5_EXPORT("Az4H9FlfxCA", sceFreeTypeOtUnknown_Az4H9FlfxCA_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_xj3tNJptfLI_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerBeginSubPath_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("xj3tNJptfLI", sceFreeTypeOtUnknown_xj3tNJptfLI_nid_no_patch); +APS5_EXPORT("xj3tNJptfLI", sceFreeTypeOtStrokerBeginSubPath_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_TPKhRt1QIeE_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerConicTo_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("TPKhRt1QIeE", sceFreeTypeOtUnknown_TPKhRt1QIeE_nid_no_patch); +APS5_EXPORT("TPKhRt1QIeE", sceFreeTypeOtStrokerConicTo_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_6_yXRlVrjbc_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerCubicTo_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("6+yXRlVrjbc", sceFreeTypeOtUnknown_6_yXRlVrjbc_nid_no_patch); +APS5_EXPORT("6+yXRlVrjbc", sceFreeTypeOtStrokerCubicTo_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown__RLa_6fbshA_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerDone_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("+RLa-6fbshA", sceFreeTypeOtUnknown__RLa_6fbshA_nid_no_patch); +APS5_EXPORT("+RLa-6fbshA", sceFreeTypeOtStrokerDone_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_pBqh_Uh_gns_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerEndSubPath_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("pBqh-Uh-gns", sceFreeTypeOtUnknown_pBqh_Uh_gns_nid_no_patch); +APS5_EXPORT("pBqh-Uh-gns", sceFreeTypeOtStrokerEndSubPath_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_2rcvKZfm5rI_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -582,40 +582,40 @@ int APS5_VABI sceFreeTypeOtUnknown_7cIS2cFG3PE_nid_no_patch() { } APS5_EXPORT("7cIS2cFG3PE", sceFreeTypeOtUnknown_7cIS2cFG3PE_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_LfyUi_LkdCk_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerGetBorderCounts_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("LfyUi-LkdCk", sceFreeTypeOtUnknown_LfyUi_LkdCk_nid_no_patch); +APS5_EXPORT("LfyUi-LkdCk", sceFreeTypeOtStrokerGetBorderCounts_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_rzIWh7FJnmU_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerGetCounts_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("rzIWh7FJnmU", sceFreeTypeOtUnknown_rzIWh7FJnmU_nid_no_patch); +APS5_EXPORT("rzIWh7FJnmU", sceFreeTypeOtStrokerGetCounts_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_cKndiZnt0EA_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerLineTo_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("cKndiZnt0EA", sceFreeTypeOtUnknown_cKndiZnt0EA_nid_no_patch); +APS5_EXPORT("cKndiZnt0EA", sceFreeTypeOtStrokerLineTo_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_eqGCaeizvWM_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerNew_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("eqGCaeizvWM", sceFreeTypeOtUnknown_eqGCaeizvWM_nid_no_patch); +APS5_EXPORT("eqGCaeizvWM", sceFreeTypeOtStrokerNew_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_RvFx7dv9ThU_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } APS5_EXPORT("RvFx7dv9ThU", sceFreeTypeOtUnknown_RvFx7dv9ThU_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_Po8kyd3DlIw_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerRewind_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Po8kyd3DlIw", sceFreeTypeOtUnknown_Po8kyd3DlIw_nid_no_patch); +APS5_EXPORT("Po8kyd3DlIw", sceFreeTypeOtStrokerRewind_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_B3uU5aDZn94_nid_no_patch() { +int APS5_VABI sceFreeTypeOtStrokerSet_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("B3uU5aDZn94", sceFreeTypeOtUnknown_B3uU5aDZn94_nid_no_patch); +APS5_EXPORT("B3uU5aDZn94", sceFreeTypeOtStrokerSet_nid_no_patch); int APS5_VABI sceFreeTypeOtTan_nid_no_patch() { NotImplemented_nid_no_patch(__func__); diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index a0f692fcaa..12c250d766 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -1,9 +1,10 @@ # libSceFreeTypeOt technical debt +- The FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy, FT_Outline_Decompose, FT_Stroker_New, FT_Stroker_Done, FT_Stroker_GetCounts, FT_Stroker_BeginSubPath, FT_Stroker_LineTo, FT_Stroker_EndSubPath, FT_Stroker_Set, FT_Stroker_Rewind, FT_Stroker_GetBorderCounts, FT_Stroker_ConicTo and FT_Stroker_CubicTo NIDs in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. +- The 129 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. ### Unknown function info - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. - The import-resolution helpers in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) retain the PS5 3.20 NIDs from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs), including identified names and still-unidentified exports. Their provider-specific prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. -- The FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy and FT_Outline_Decompose NIDs in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. From ca67aefde9beb2ce36d4b383c9eb590ea8370ded Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:11:08 +0000 Subject: [PATCH 456/702] refactor(libSceFreeType): declare the exports of this PR in the public header The public header audit wants one declaration per export in the library header, so Api.hpp declares them with their definitions' signatures and the tests include it instead of their own extern blocks. --- core/libs/prx/libSceFreeType/include/Api.hpp | 11 +++++++++++ core/libs/prx/libSceFreeType/src/Stroker.cpp | 1 + core/libs/tests/GuestFreeTypeStroker.cpp | 15 +-------------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 048fa59647..1135668bad 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -82,5 +82,16 @@ std::int64_t APS5_VABI FT_Sin_nid_postfix(std::int64_t angle); std::int64_t APS5_VABI FT_Tan_nid_postfix(std::int64_t angle); void APS5_VABI FT_Vector_Unit_nid_postfix(FreeType::GuestVector* vector, std::int64_t angle); std::int64_t APS5_VABI FT_Angle_Diff_nid_postfix(std::int64_t first, std::int64_t second); +int APS5_VABI FT_Stroker_GetCounts_nid_postfix(void* stroker, std::uint32_t* points, std::uint32_t* contours); +int APS5_VABI FT_Stroker_CubicTo_nid_postfix(void* stroker, const FreeType::GuestVector* control1, const FreeType::GuestVector* control2, const FreeType::GuestVector* to); +void APS5_VABI FT_Stroker_Done_nid_postfix(void* stroker); +int APS5_VABI FT_Stroker_GetBorderCounts_nid_postfix(void* stroker, std::int32_t border, std::uint32_t* points, std::uint32_t* contours); +int APS5_VABI FT_Stroker_EndSubPath_nid_postfix(void* stroker); +int APS5_VABI FT_Stroker_LineTo_nid_postfix(void* stroker, const FreeType::GuestVector* to); +int APS5_VABI FT_Stroker_New_nid_postfix(FreeType::GuestLibrary* library, void** stroker); +void APS5_VABI FT_Stroker_Set_nid_postfix(void* stroker, std::int64_t radius, std::int32_t lineCap, std::int32_t lineJoin, std::int64_t miterLimit); +void APS5_VABI FT_Stroker_Rewind_nid_postfix(void* stroker); +int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void* stroker, const FreeType::GuestVector* to, std::uint8_t open); +int APS5_VABI FT_Stroker_ConicTo_nid_postfix(void* stroker, const FreeType::GuestVector* control, const FreeType::GuestVector* to); } diff --git a/core/libs/prx/libSceFreeType/src/Stroker.cpp b/core/libs/prx/libSceFreeType/src/Stroker.cpp index c733f67a72..ca2fe64191 100644 --- a/core/libs/prx/libSceFreeType/src/Stroker.cpp +++ b/core/libs/prx/libSceFreeType/src/Stroker.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceFreeType/include/Api.hpp" #include "prx/libSceFreeType/include/GuestObjects.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" diff --git a/core/libs/tests/GuestFreeTypeStroker.cpp b/core/libs/tests/GuestFreeTypeStroker.cpp index 2d86ca21a8..55e5567685 100644 --- a/core/libs/tests/GuestFreeTypeStroker.cpp +++ b/core/libs/tests/GuestFreeTypeStroker.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceFreeType/include/Api.hpp" #include "prx/libSceFreeType/include/GuestObjects.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -9,20 +10,6 @@ using namespace FreeType; -extern "C" { -int APS5_VABI FT_Stroker_New_nid_postfix(GuestLibrary*, void**); -void APS5_VABI FT_Stroker_Done_nid_postfix(void*); -int APS5_VABI FT_Stroker_GetCounts_nid_postfix(void*, std::uint32_t*, std::uint32_t*); -int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void*, const GuestVector*, std::uint8_t); -int APS5_VABI FT_Stroker_LineTo_nid_postfix(void*, const GuestVector*); -int APS5_VABI FT_Stroker_EndSubPath_nid_postfix(void*); -void APS5_VABI FT_Stroker_Set_nid_postfix(void*, std::int64_t, std::int32_t, std::int32_t, std::int64_t); -void APS5_VABI FT_Stroker_Rewind_nid_postfix(void*); -int APS5_VABI FT_Stroker_GetBorderCounts_nid_postfix(void*, std::int32_t, std::uint32_t*, std::uint32_t*); -int APS5_VABI FT_Stroker_ConicTo_nid_postfix(void*, const GuestVector*, const GuestVector*); -int APS5_VABI FT_Stroker_CubicTo_nid_postfix(void*, const GuestVector*, const GuestVector*, const GuestVector*); -} - static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "FreeType stroker check failed at line %d\n", line); From c55d34663a7c7312924b59e87df89d9fc8b0b110 Mon Sep 17 00:00:00 2001 From: BiladeiroSupremo <ag.rio@hotmail.com> Date: Thu, 15 Oct 2026 09:52:19 -0300 Subject: [PATCH 457/702] fix(libkernel): map AMM pages from a fragmented pool _ammMap took one contiguous run of the direct memory given to the mapper and threw (an uncaught runtime_error -> abort) when the pool was fragmented: PPSA03671 aborted ~4 min into a slot-4 load on an 11 MB map ("AMM: no free run of 11010048 bytes"). The PS5 maps the mapper's pages through page tables, so a fragmented pool still serves the map: when no run fits, the map now takes pages from several runs (first fit) and maps each contiguous group. Run hww8: the 11 MB map and several 10-33 MB maps went through, no abort. --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 59 ++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index dda99c8fbf..84074a2d8d 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -346,16 +346,43 @@ void _returnPoolPages(AmmState& state, std::uint64_t offset, std::uint64_t bytes state.pool.emplace(offset, bytes); } -std::uint64_t _takePoolPages(AmmState& state, std::uint64_t bytes) { +bool _tryTakePoolPages(AmmState& state, std::uint64_t bytes, std::uint64_t& offset) { for (auto it = state.pool.begin(); it != state.pool.end(); ++it) { if (it->second < bytes) continue; - const auto offset = it->first; + offset = it->first; const auto remaining = it->second - bytes; state.pool.erase(it); if (remaining != 0) state.pool.emplace(offset + bytes, remaining); - return offset; + return true; } - throw std::runtime_error("AMM: no free run of " + std::to_string(bytes) + " bytes in the direct memory given to the mapper"); + return false; +} + +std::uint64_t _takePoolPages(AmmState& state, std::uint64_t bytes) { + std::uint64_t offset = 0; + if (!_tryTakePoolPages(state, bytes, offset)) throw std::runtime_error("AMM: no free run of " + std::to_string(bytes) + " bytes in the direct memory given to the mapper"); + return offset; +} + +// Pool pages for `bytes` taken from several runs (first fit), for a map no single free run holds: +// the PS5 maps the mapper's pages through page tables, so a fragmented pool still serves it +// (PPSA03671 aborted on an 11 MB map ~4 min into a slot-4 load with only contiguous runs). +std::vector<AmmPage> _takeScatteredPages(AmmState& state, std::uint64_t bytes, std::int32_t type, std::int32_t protection) { + std::uint64_t free = 0; + for (const auto& [offset, size] : state.pool) free += size; + if (free < bytes) throw std::runtime_error("AMM: " + std::to_string(bytes) + " bytes requested, " + std::to_string(free) + " free in the direct memory given to the mapper"); + std::vector<AmmPage> pages; + pages.reserve(bytes / PS5_PAGE_SIZE); + while (pages.size() * PS5_PAGE_SIZE < bytes) { + auto it = state.pool.begin(); + const auto take = std::min(it->second, bytes - pages.size() * PS5_PAGE_SIZE); + const auto offset = it->first; + const auto remaining = it->second - take; + state.pool.erase(it); + if (remaining != 0) state.pool.emplace(offset + take, remaining); + for (std::uint64_t page = 0; page < take; page += PS5_PAGE_SIZE) pages.push_back({offset + page, type, protection, true}); + } + return pages; } void _addPage(AmmState& state, std::uint64_t address, const AmmPage& page) { @@ -511,7 +538,29 @@ void _ammMap(const Apr::AmmMapCommand& command, bool direct) { _reserve(command.address, command.size); _forgetPages(state, command.address, command.size); _clearPrt(state, command.address, command.size); - const auto physical = direct ? command.directOffset : _takePoolPages(state, command.size); + std::uint64_t physical = direct ? command.directOffset : 0; + if (!direct && !_tryTakePoolPages(state, command.size, physical)) { + const auto pages = _takeScatteredPages(state, command.size, command.type, command.protection); + static std::atomic<int> reported{0}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 8) std::fprintf(stderr, "[amm] no free run of %llu bytes: mapping 0x%llx from scattered pool pages\n", static_cast<unsigned long long>(command.size), static_cast<unsigned long long>(command.address)); + std::size_t mapped = 0; + try { + for (std::size_t first = 0; first < pages.size();) { + std::size_t last = first + 1; + while (last < pages.size() && pages[last].physical == pages[last - 1].physical + PS5_PAGE_SIZE) ++last; + _mapRun(command.address + first * PS5_PAGE_SIZE, pages[first].physical, (last - first) * PS5_PAGE_SIZE, command.protection); + for (std::size_t index = first; index < last; ++index) _addPage(state, command.address + index * PS5_PAGE_SIZE, pages[index]); + mapped = last; + first = last; + } + } catch (...) { + _forgetPages(state, command.address, command.size); + for (std::size_t index = mapped; index < pages.size(); ++index) _returnPoolPages(state, pages[index].physical, PS5_PAGE_SIZE); + _reserve(command.address, command.size); + throw; + } + return; + } try { _mapRun(command.address, physical, command.size, command.protection); } catch (...) { From ba6e914e1dbd6fcc28540e8eb9fd20d9389429b4 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 22:04:03 +1300 Subject: [PATCH 458/702] fix(libkernel): drop the comment and log line from the scattered AMM map The exhaustion message stays "AMM: no free run of N bytes" when the pool holds fewer bytes than the map needs. --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 84074a2d8d..4b266b0f68 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -364,13 +364,10 @@ std::uint64_t _takePoolPages(AmmState& state, std::uint64_t bytes) { return offset; } -// Pool pages for `bytes` taken from several runs (first fit), for a map no single free run holds: -// the PS5 maps the mapper's pages through page tables, so a fragmented pool still serves it -// (PPSA03671 aborted on an 11 MB map ~4 min into a slot-4 load with only contiguous runs). std::vector<AmmPage> _takeScatteredPages(AmmState& state, std::uint64_t bytes, std::int32_t type, std::int32_t protection) { std::uint64_t free = 0; for (const auto& [offset, size] : state.pool) free += size; - if (free < bytes) throw std::runtime_error("AMM: " + std::to_string(bytes) + " bytes requested, " + std::to_string(free) + " free in the direct memory given to the mapper"); + if (free < bytes) throw std::runtime_error("AMM: no free run of " + std::to_string(bytes) + " bytes in the direct memory given to the mapper"); std::vector<AmmPage> pages; pages.reserve(bytes / PS5_PAGE_SIZE); while (pages.size() * PS5_PAGE_SIZE < bytes) { @@ -541,8 +538,6 @@ void _ammMap(const Apr::AmmMapCommand& command, bool direct) { std::uint64_t physical = direct ? command.directOffset : 0; if (!direct && !_tryTakePoolPages(state, command.size, physical)) { const auto pages = _takeScatteredPages(state, command.size, command.type, command.protection); - static std::atomic<int> reported{0}; - if (reported.fetch_add(1, std::memory_order_relaxed) < 8) std::fprintf(stderr, "[amm] no free run of %llu bytes: mapping 0x%llx from scattered pool pages\n", static_cast<unsigned long long>(command.size), static_cast<unsigned long long>(command.address)); std::size_t mapped = 0; try { for (std::size_t first = 0; first < pages.size();) { From 0344e6e20995b9ad381c24a5888067c6cbf4cb6d Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 22:04:03 +1300 Subject: [PATCH 459/702] test(libkernel): map AMM pages from a fragmented pool Fragment the pool into single free pages, map a range no single run holds, check each page keeps its own data, and check an exhausted pool still reports no free run. --- core/libs/tests/GuestAmpr.cpp | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 2795ed3172..f2394f5687 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -1125,6 +1125,53 @@ void TestAmmRemapKeepsPoolReferences() { Require(At(fresh) == 0xC1); } +void TestAmmMapsScatteredPoolPages() { + 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); + const auto exhausts = [](Recorder& recorder) { + try { + SubmitAmm(recorder); + } catch (const std::runtime_error& error) { + return std::string(error.what()).starts_with("AMM: no free run of "); + } + return false; + }; + const auto drained = start + 1024 * page; + for (std::uint64_t index = 0;; ++index) { + Require(index < 4096); + Recorder drain; + Require(sceAmprAmmCommandBufferMap(&drain.buffer, drained + index * page, page, 0, cpuReadWrite) == 0); + if (exhausts(drain)) break; + } + + std::int64_t pool = -1; + Require(sceAmprAmmGiveDirectMemory(0, static_cast<std::int64_t>(sceKernelGetDirectMemorySize()), 5 * page, page, 1, &pool) == 0); + const auto singles = start + 6144 * page; + Recorder maps; + for (std::uint64_t index = 0; index < 5; ++index) Require(sceAmprAmmCommandBufferMap(&maps.buffer, singles + index * page, page, 0, cpuReadWrite) == 0); + SubmitAmm(maps); + for (std::uint64_t index = 0; index < 5; ++index) At(singles + index * page) = 0x100 + index; + + Recorder release; + for (const std::uint64_t index : {0, 2, 4}) Require(sceAmprAmmCommandBufferUnmap(&release.buffer, singles + index * page, page) == 0); + SubmitAmm(release); + + const auto scattered = start + 6400 * page; + Recorder gather; + Require(sceAmprAmmCommandBufferMap(&gather.buffer, scattered, 3 * page, 0, cpuReadWrite) == 0); + SubmitAmm(gather); + for (std::uint64_t index = 0; index < 3; ++index) At(scattered + index * page) = 0x200 + index; + for (std::uint64_t index = 0; index < 3; ++index) Require(At(scattered + index * page) == 0x200 + index); + Require(At(singles + page) == 0x101 && At(singles + 3 * page) == 0x103); + + Recorder empty; + Require(sceAmprAmmCommandBufferMap(&empty.buffer, singles, page, 0, cpuReadWrite) == 0); + Require(exhausts(empty)); +} + void TestAmmMapFailureKeepsPoolOwnership() { std::uint64_t start = 0; std::uint64_t end = 0; @@ -1467,6 +1514,7 @@ int main() { TestAmmUsageStats(); TestAmmRemapKeepsPoolReferences(); TestAmmMapFailureKeepsPoolOwnership(); + TestAmmMapsScatteredPoolPages(); TestAmm(); TestAmmRemapAndProtect(); TestAmmPrt(); From 9e6c5921a0043526fca0e8585da8e71f02fe5f86 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:06:08 +0000 Subject: [PATCH 460/702] docs(debt): drop the blank line left at the end of the debt file --- docs/dev/debt/libSceFreeTypeOt.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 12c250d766..e85f1ace93 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -7,4 +7,3 @@ - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. - The import-resolution helpers in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) retain the PS5 3.20 NIDs from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs), including identified names and still-unidentified exports. Their provider-specific prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. - From 1bdb8388ed80aa27169df490462b1bf039067ae4 Mon Sep 17 00:00:00 2001 From: guglia001 <7321539+guglia001@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:52:23 +0100 Subject: [PATCH 461/702] test(relinker): add a libFuzzer target for the whole conversion ANYPS5_BUILD_FUZZERS builds relinker_conversion_fuzzer with AddressSanitizer and UndefinedBehaviorSanitizer in relinker-only Clang builds on Linux. The first input byte selects the target system, --to-intel and the unused-filter level; the rest is the executable passed to Conversion::Convert, the path the web converter takes. tools/relinker_fuzz_seeds.py writes the relinker test fixtures as seeds, and CTest runs a short smoke run. --- CMakeLists.txt | 13 +++++- core/relinker/CMakeLists.txt | 10 +++++ core/relinker/fuzz/ConversionFuzzer.cpp | 40 ++++++++++++++++++ docs/dev/BUILD.md | 13 ++++++ tools/relinker_fuzz_seeds.py | 55 +++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 core/relinker/fuzz/ConversionFuzzer.cpp create mode 100644 tools/relinker_fuzz_seeds.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 7db1bb7432..5e3e462530 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,7 +89,18 @@ if(ANYPS5_ENABLE_TSAN) endif() endif() -if(ANYPS5_ENABLE_ASAN) +option(ANYPS5_BUILD_FUZZERS "Build the relinker libFuzzer targets with AddressSanitizer and UndefinedBehaviorSanitizer" OFF) +if(ANYPS5_BUILD_FUZZERS) + if(NOT ANYPS5_RELINKER_ONLY) + message(FATAL_ERROR "ANYPS5_BUILD_FUZZERS requires ANYPS5_RELINKER_ONLY") + endif() + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR APPLE OR WIN32) + message(FATAL_ERROR "ANYPS5_BUILD_FUZZERS requires Clang on Linux") + endif() + set(ANYPS5_RELINKER_CLI OFF CACHE BOOL "Build the native relinker command line" FORCE) + add_compile_options(-fsanitize=address,undefined,fuzzer-no-link -fno-sanitize=function -fno-sanitize-recover=undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) +elseif(ANYPS5_ENABLE_ASAN) if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux") endif() diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2267be074f..f7dbec4b98 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -130,6 +130,16 @@ if(BUILD_TESTING) set_tests_properties(call_registry_writer PROPERTIES TIMEOUT 20) endif() +if(ANYPS5_BUILD_FUZZERS) + add_test_executable(relinker_conversion_fuzzer ${CMAKE_CURRENT_SOURCE_DIR}/fuzz/ConversionFuzzer.cpp) + target_link_libraries(relinker_conversion_fuzzer PRIVATE relinker_core) + target_link_options(relinker_conversion_fuzzer PRIVATE -fsanitize=fuzzer) + if(BUILD_TESTING) + add_test(NAME relinker_conversion_fuzzer COMMAND relinker_conversion_fuzzer -runs=20000 -seed=1) + set_tests_properties(relinker_conversion_fuzzer PROPERTIES TIMEOUT 120) + endif() +endif() + if(ANYPS5_BUILD_WASM) target_compile_options(relinker_core PRIVATE -fexceptions) add_subdirectory(wasm) diff --git a/core/relinker/fuzz/ConversionFuzzer.cpp b/core/relinker/fuzz/ConversionFuzzer.cpp new file mode 100644 index 0000000000..234d14857b --- /dev/null +++ b/core/relinker/fuzz/ConversionFuzzer.cpp @@ -0,0 +1,40 @@ +#include <relinker/pipeline/Conversion.hpp> +#include <io/NativePath.hpp> +#include <cstddef> +#include <cstdint> +#include <exception> +#include <string_view> + +namespace { + +class SilentProgress final : public Domain::IProgress { +public: + void Report(const Domain::ProgressEvent&) override {} + void Message(std::string_view) override {} +}; + +} + +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) { + if (size < 1) return 0; + const std::uint8_t selector = data[0]; + Relinker::ConversionInput input; + input.InputPath = Io::NativePath("/input/eboot.bin"); + input.OutputPath = Io::NativePath("/output/eboot"); + input.ModuleRoot = "/input"; + input.Executable.assign(data + 1, data + size); + Relinker::ConversionOptions options; + switch (selector % 3) { + case 0: options.Target = Relinker::TargetSystem::Linux; break; + case 1: options.Target = Relinker::TargetSystem::Windows; break; + default: options.Target = Relinker::TargetSystem::MacOs; break; + } + options.ToIntel = (selector & 0x04) != 0; + options.UnusedFilterLevel = (selector >> 3) % 3; + SilentProgress progress; + try { + Relinker::Conversion(progress).Convert(std::move(input), options); + } catch (const std::exception&) { + } + return 0; +} diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 98784404a0..4b8e281865 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -18,6 +18,19 @@ The output is x86-64: Linux ELF by default, Windows PE with `--windows` or Mach- Use a separate build directory for the full build. +## Fuzzing + +`ANYPS5_BUILD_FUZZERS` builds `relinker_conversion_fuzzer`, a libFuzzer target that runs the whole conversion on its input, as the web converter does. It requires `ANYPS5_RELINKER_ONLY`, Clang on Linux and the compiler-rt sanitizer runtimes (`libclang-rt-18-dev` on Ubuntu 24.04). Every target is built with AddressSanitizer and UndefinedBehaviorSanitizer, without `-fsanitize=function`: that check reads a signature before the target of every indirect call and faults on the generated code the codegen tests call. The first input byte selects the target system (modulo 3: Linux, Windows, macOS), bit 2 `--to-intel` and the byte shifted right by 3, modulo 3, the `unused-filter` level; the remaining bytes are the executable. A conversion that throws a `std::exception` is a rejected input; a sanitizer report, an abort or a timeout is a finding. + +```sh +cmake -S . -B build-fuzz -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DANYPS5_RELINKER_ONLY=ON -DANYPS5_BUILD_FUZZERS=ON -DBUILD_TESTING=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ +cmake --build build-fuzz --target relinker_conversion_fuzzer +python3 tools/relinker_fuzz_seeds.py build-fuzz/fuzz-corpus +build-fuzz/tests/relinker_conversion_fuzzer build-fuzz/fuzz-corpus -fork=2 -max_total_time=1200 -timeout=20 +``` + +`relinker_fuzz_seeds.py` writes the executables built by the relinker's Python tests as seeds, one per target system. With `BUILD_TESTING=ON`, CTest runs 20000 inputs from an empty corpus as `relinker_conversion_fuzzer`. Findings are written to the working directory as `crash-*`, `timeout-*` or `oom-*`; reproduce one with `build-fuzz/tests/relinker_conversion_fuzzer <file>`. + ## Full build ```sh diff --git a/tools/relinker_fuzz_seeds.py b/tools/relinker_fuzz_seeds.py new file mode 100644 index 0000000000..87cefa2990 --- /dev/null +++ b/tools/relinker_fuzz_seeds.py @@ -0,0 +1,55 @@ +import argparse +import importlib +import inspect +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +TESTS = ROOT / "core" / "relinker" / "relinker" / "tests" +BUILDERS = ("fixture", "image", "make_image", "tls_image", "padded_image", "executable_image", + "empty_tls_image", "make_eof_segment_prefix_image") +SELECTORS = (0, 1, 2, 4) + + +def without_arguments(function): + for parameter in inspect.signature(function).parameters.values(): + positional = parameter.kind in (parameter.POSITIONAL_ONLY, parameter.POSITIONAL_OR_KEYWORD) + if positional and parameter.default is inspect.Parameter.empty: + return False + return True + + +def images(): + sys.path.insert(0, str(TESTS)) + for path in sorted(TESTS.glob("test_*.py")): + try: + module = importlib.import_module(path.stem) + except Exception: + continue + for name in BUILDERS: + function = getattr(module, name, None) + if not callable(function) or not without_arguments(function): + continue + try: + data = function() + except Exception: + continue + if isinstance(data, (bytes, bytearray)) and len(data) >= 64: + yield f"{path.stem}-{name}", bytes(data) + + +def main(): + parser = argparse.ArgumentParser(description="Write relinker test fixtures as relinker_conversion_fuzzer seeds") + parser.add_argument("output", type=Path) + arguments = parser.parse_args() + arguments.output.mkdir(parents=True, exist_ok=True) + count = 0 + for name, data in images(): + for selector in SELECTORS: + (arguments.output / f"{name}-{selector}").write_bytes(bytes([selector]) + data) + count += 1 + print(f"{count} seeds written to {arguments.output}") + + +if __name__ == "__main__": + main() From e8db1c1648ab40b33e6e5509461856a0fcf19f38 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar <santhosh-pc@outlook.com> Date: Tue, 27 Oct 2026 14:45:53 +0530 Subject: [PATCH 462/702] test(libSceLibcInternal): observe combined spawn flags in a dedicated fixture --- .../prx/libSceLibcInternal/CMakeLists.txt | 9 +++ core/libs/tests/GuestPosixSpawnFlags.cpp | 68 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 core/libs/tests/GuestPosixSpawnFlags.cpp diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index f17760d179..571bde9b04 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -89,6 +89,15 @@ if(BUILD_TESTING) configure_windows_unwind(guest_program_name_tests) add_test(NAME guest_program_name COMMAND guest_program_name_tests) set_tests_properties(guest_program_name PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_posix_spawn_flags_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSpawnFlags.cpp) + target_include_directories(guest_posix_spawn_flags_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_spawn_flags_tests PRIVATE libSceLibcInternal libc libkernel ${CMAKE_DL_LIBS}) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/spawn-flags) + add_test(NAME guest_posix_spawn_flags COMMAND guest_posix_spawn_flags_tests) + set_tests_properties(guest_posix_spawn_flags PROPERTIES TIMEOUT 20 + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/spawn-flags) + endif() if(WIN32) set_tests_properties(guest_program_name PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibcInternal>") diff --git a/core/libs/tests/GuestPosixSpawnFlags.cpp b/core/libs/tests/GuestPosixSpawnFlags.cpp new file mode 100644 index 0000000000..8893cdddf7 --- /dev/null +++ b/core/libs/tests/GuestPosixSpawnFlags.cpp @@ -0,0 +1,68 @@ +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include <atomic> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <dlfcn.h> +#include <filesystem> +#include <fstream> +#include <spawn.h> +#include <unistd.h> + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "Posix spawn flags check failed at line %d\n", line); + std::abort(); + } +} +#define Check(value) Require((value), __LINE__) + +static std::atomic<int> observedSpawnFlags{-1}; + +extern "C" int posix_spawn(pid_t* process, const char* path, const posix_spawn_file_actions_t* actions, + const posix_spawnattr_t* attributes, char* const arguments[], char* const environment[]) { + short flags = 0; + Check(attributes == nullptr || posix_spawnattr_getflags(attributes, &flags) == 0); + static const auto spawn = reinterpret_cast<decltype(&posix_spawn)>(dlsym(RTLD_NEXT, "posix_spawn")); + Check(spawn != nullptr); + observedSpawnFlags.store(flags, std::memory_order_relaxed); + return spawn(process, path, actions, attributes, arguments, environment); +} + +int main(int count, char** arguments) { + if (count == 2 && std::strcmp(arguments[1], "spawn-flags-child") == 0) return 43; + std::filesystem::create_directories("app0"); + { + std::ofstream image("app0/eboot.bin"); + image << "image"; + Check(image.good()); + } + PosixSpawnAttributes* attributes = nullptr; + Check(posix_spawnattr_init_nid_postfix(&attributes) == 0); + Check(posix_spawnattr_setpgroup_nid_postfix(&attributes, 0) == 0); + constexpr int expectedFlags[] = {0, POSIX_SPAWN_RESETIDS, POSIX_SPAWN_SETPGROUP, + POSIX_SPAWN_RESETIDS | POSIX_SPAWN_SETPGROUP}; + char program[] = "/app0/eboot.bin"; + char role[] = "spawn-flags-child"; + char* childArguments[] = {program, role, nullptr}; + for (std::int16_t flags = 0; flags < 4; ++flags) { + Check(posix_spawnattr_setflags_nid_postfix(&attributes, flags) == 0); + observedSpawnFlags.store(-1, std::memory_order_relaxed); + std::int32_t process = -1; + Check(posix_spawn_nid_postfix(&process, program, nullptr, &attributes, childArguments, nullptr) == 0); + Check(process > 0); + const int observed = observedSpawnFlags.load(std::memory_order_relaxed); + if (observed != expectedFlags[flags]) + std::fprintf(stderr, "Spawn flags %d: expected host flags %d, observed %d\n", flags, expectedFlags[flags], observed); + Check(observed == expectedFlags[flags]); + Check(getpgid(process) == ((flags & 2) != 0 ? process : getpgrp())); + int status = -1; + Check(waitpid_nid_postfix(process, &status, 0) == process); + Check((status & 0x7f) == 0 && ((status >> 8) & 0xff) == 43); + } + Check(posix_spawnattr_destroy_nid_postfix(&attributes) == 0); + Check(std::filesystem::remove("app0/eboot.bin")); + Check(std::filesystem::remove("app0")); +} From d3d7ae0ae2946e9d539dfe85aa8bc43480254de2 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar <santhosh-pc@outlook.com> Date: Tue, 27 Oct 2026 14:48:12 +0530 Subject: [PATCH 463/702] fix(libSceLibcInternal): preserve combined POSIX spawn flags --- core/libs/prx/libSceLibcInternal/Spawn.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp index 4c7bd815b2..3d26b19519 100644 --- a/core/libs/prx/libSceLibcInternal/Spawn.cpp +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -393,11 +393,13 @@ int SpawnChild(const char* function, const std::string& guest, PosixSpawnFileAct if (flags != 0) { if (posix_spawnattr_init(&hostAttributes) != 0) return Fault; usedAttributes = &hostAttributes; - if ((flags & SpawnFlagResetIds) != 0) posix_spawnattr_setflags(usedAttributes, POSIX_SPAWN_RESETIDS); + short hostFlags = 0; + if ((flags & SpawnFlagResetIds) != 0) hostFlags |= POSIX_SPAWN_RESETIDS; if ((flags & SpawnFlagSetGroup) != 0) { - posix_spawnattr_setflags(usedAttributes, POSIX_SPAWN_SETPGROUP); + hostFlags |= POSIX_SPAWN_SETPGROUP; posix_spawnattr_setpgroup(usedAttributes, (*attributes)->processGroup); } + posix_spawnattr_setflags(usedAttributes, hostFlags); } posix_spawn_file_actions_t hostActions; posix_spawn_file_actions_t* usedActions = nullptr; From 1c7f19e7166f42cdd17ac7eef7e3cb58b3d89c9c Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Mon, 19 Oct 2026 17:04:35 -0400 Subject: [PATCH 464/702] fix(agc): pin the import ceiling only on memory exhaustion A failed vkAllocateMemory still makes the host import budget follow the driver's VK_EXT_memory_budget report, but only VK_ERROR_OUT_OF_HOST_MEMORY and VK_ERROR_OUT_OF_DEVICE_MEMORY pin the permanent ceiling. RADV reports a refused user-memory import as VK_ERROR_UNKNOWN or VK_ERROR_INVALID_EXTERNAL_HANDLE, and pinning on those collapsed the budget to the few MiB live at the time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Graphics/src/GuestBufferMemory.cpp | 3 +- .../libSceAgcDriver/tests/BdaResources.cpp | 58 ++++++++++++++++--- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 00510dabcd..c9886fddc6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2162,9 +2162,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import GuestArena::GuestArenaUnmapAlias_nid_postfix(std::exchange(entry.alias, nullptr)); #endif std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies, requested by %s\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), step, static_cast<int>(result), ImportRequesterName(requester)); + const bool exhausted = result == VK_ERROR_OUT_OF_HOST_MEMORY || result == VK_ERROR_OUT_OF_DEVICE_MEMORY; if (std::string_view(step) == "vkAllocateMemory") { state.allocationFailed = true; - if (++state.allocationFailures >= 3 || state.liveBytes + bytes + budget / 8 >= budget) { + if (exhausted && (++state.allocationFailures >= 3 || state.liveBytes + bytes + budget / 8 >= budget)) { state.ceiling = std::max<std::uint64_t>(state.liveBytes, 1); std::fprintf(stderr, "[gpu] host import budget lowered to the %.1f MiB live after vkAllocateMemory failed %s\n", state.liveBytes / 1048576.0, state.allocationFailures >= 3 ? "3 times in a row" : "within an eighth of the budget"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 55a7cfe408..b193337376 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1868,6 +1868,42 @@ void importDeviceBudgetTests() { Require(HostImportBudgetWith(5888 * MiB, false, 0, true, 0) != 0, "a host heap report that leaves imports no room made the default import budget 0, which the refusal memory reads as no budget"); } +void importAllocationErrorTests(const Context& context, VkResult failure, bool nearBudget) { + constexpr std::size_t piece = 65536; + constexpr std::size_t count = 8; + auto* block = static_cast<std::uint8_t*>(::operator new(2 * count * piece, std::align_val_t{piece})); + auto importing = context; + importing.hostImportAlignment = piece; + ImportAllocationProbe probe(importing); + SetHostImportWindow(0); + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(block + 2 * index * piece, piece, true, true, true); + } + SetHostImportBudget((nearBudget ? 2 : 64) * piece); + std::size_t next = 0; + const auto lookup = [&] { return HostImportFor(importing, reinterpret_cast<std::uintptr_t>(block + 2 * next++ * piece), 16) != nullptr; }; + const auto counted = [] { return HostImportCounters().requesters[static_cast<std::size_t>(ImportRequester::Access)]; }; + Require(lookup(), "the allocation error test's first import was not made"); + probe.failure = failure; + const auto before = counted(); + for (int attempt = 0; attempt < 3; ++attempt) Require(!lookup(), "an import with an injected allocation failure was made"); + const auto after = counted(); + probe.failure = VK_SUCCESS; + const bool exhausted = failure == VK_ERROR_OUT_OF_DEVICE_MEMORY || failure == VK_ERROR_OUT_OF_HOST_MEMORY; + const auto failed = exhausted && nearBudget ? 1u : 3u; + Require(after.failed - before.failed == failed && after.refused - before.refused == 3 - failed, "allocation error " + std::to_string(failure) + " changed the import failure/refusal counts"); + Require(lookup() != exhausted, "allocation error " + std::to_string(failure) + " incorrectly changed the host import budget"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Remove(block + 2 * index * piece); + } + ClearHostImports(importing.device); + SetHostImportBudget(0); + ::operator delete(block, std::align_val_t{piece}); +} + void importCeilingTests(const Context& context, const BdaTestAccess& access, bool nearBudget) { constexpr std::size_t piece = 65536; constexpr std::size_t count = 24; @@ -1931,7 +1967,7 @@ void VKAPI_PTR reportHostHeap(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties } } -void importReportFloorTests(const Context& context, const BdaTestAccess& access, bool setBudget) { +void importReportFloorTests(const Context& context, bool setBudget, VkResult failure) { constexpr std::size_t piece = 65536; constexpr std::size_t count = 24; constexpr std::uint64_t GiB = std::uint64_t{1} << 30u; @@ -1939,6 +1975,7 @@ void importReportFloorTests(const Context& context, const BdaTestAccess& access, auto* block = static_cast<std::uint8_t*>(::operator new(2 * count * piece, std::align_val_t{piece})); auto importing = context; importing.hostImportAlignment = piece; + ImportAllocationProbe probe(importing); importing.memory.memoryHeapCount = 2; importing.memory.memoryHeaps[0] = {8 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; importing.memory.memoryHeaps[1] = {16 * GiB, 0}; @@ -1957,11 +1994,10 @@ void importReportFloorTests(const Context& context, const BdaTestAccess& access, const auto lookup = [&] { return HostImportFor(importing, reinterpret_cast<std::uintptr_t>(block + 2 * next++ * piece), 16) != nullptr; }; const std::string budget = setBudget ? "a set import budget" : "the default import budget"; for (int made = 0; made < 3; ++made) Require(lookup(), budget + " was lowered below " + std::to_string(made + 1) + " imports by a host heap report that leaves imports no room, before any vkAllocateMemory failure"); - access.limitImports(0); - Require(!lookup(), "an import past the mock's import limit was made"); - access.limitImports(std::nullopt); - if (setBudget) Require(lookup(), "a set import budget took the host heap report's limit after a vkAllocateMemory failure far below it"); - else Require(!lookup(), "the default import budget did not take the host heap report's limit after a vkAllocateMemory failure"); + probe.failure = failure; + Require(!lookup(), "an import with an injected allocation failure was made"); + probe.failure = VK_SUCCESS; + Require(lookup() == setBudget, budget + " incorrectly applied the host heap report after allocation error " + std::to_string(failure)); SetHostImportBudget(0); { GuestAllocations::Mutation mutation; @@ -4288,8 +4324,14 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importDeviceBudgetTests(); importCeilingTests(context, access, false); importCeilingTests(context, access, true); - importReportFloorTests(context, access, true); - importReportFloorTests(context, access, false); + for (const auto failure : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_INVALID_EXTERNAL_HANDLE, VK_ERROR_UNKNOWN}) { + importAllocationErrorTests(context, failure, false); + importAllocationErrorTests(context, failure, true); + } + for (const auto failure : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_INVALID_EXTERNAL_HANDLE, VK_ERROR_UNKNOWN}) { + importReportFloorTests(context, true, failure); + importReportFloorTests(context, false, failure); + } importReportRefusalMemoryTests(context, access); sparseImportedHeapTests(context, access); heapMirrorTests(context, access); From f54ec181a11395659a14445e36160039307550e0 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 22:26:02 +1300 Subject: [PATCH 465/702] build: restore submodule pointers to main --- 3rdparty/SPIRV-Headers | 2 +- 3rdparty/SPIRV-Tools | 2 +- 3rdparty/vorbis | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/3rdparty/SPIRV-Headers b/3rdparty/SPIRV-Headers index 01e0577914..b824a462d4 160000 --- a/3rdparty/SPIRV-Headers +++ b/3rdparty/SPIRV-Headers @@ -1 +1 @@ -Subproject commit 01e0577914a75a2569c846778c2f93aa8e6feddd +Subproject commit b824a462d4256d720bebb40e78b9eb8f78bbb305 diff --git a/3rdparty/SPIRV-Tools b/3rdparty/SPIRV-Tools index 7f2d9ee926..262bdab481 160000 --- a/3rdparty/SPIRV-Tools +++ b/3rdparty/SPIRV-Tools @@ -1 +1 @@ -Subproject commit 7f2d9ee926f98fc77a3ed1e1e0f113b8c9c49458 +Subproject commit 262bdab48146c937467f826699a40da0fdfc0f1a diff --git a/3rdparty/vorbis b/3rdparty/vorbis index 0657aee69d..c2aa86b05e 160000 --- a/3rdparty/vorbis +++ b/3rdparty/vorbis @@ -1 +1 @@ -Subproject commit 0657aee69dec8508a0011f47f3b69d7538e9d262 +Subproject commit c2aa86b05e981c96bf381fc6aa11cdd03eccc2fb From f9613a9136bbac408030a8ee8a2382849da17a63 Mon Sep 17 00:00:00 2001 From: avnish <avnish0807saini@gmail.com> Date: Tue, 27 Oct 2026 15:02:46 +0530 Subject: [PATCH 466/702] test(libSceWebBrowserDialog): cover post-terminate argument validation --- core/libs/tests/GuestWebBrowserDialog.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index 53ce0d3955..a103ce8ea7 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -127,5 +127,12 @@ int main() { Require(result.padding == 0x5a5a5a5a && result.extended == &extended); Require(extended.type == 0x5a5a5a5a && extended.content[2047] == 'x'); Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogOpen(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); return 0; } From e8ed0e95df2f9182d6422ce5b862a65f29f177e2 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 22:43:09 +1300 Subject: [PATCH 467/702] test(agc): pass the legacy request version messages as plain strings require takes a C string; the version 46 and 47 depth export messages were built with std::string and did not compile. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 3b2b3fcade..5f1c1998a4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5992,7 +5992,8 @@ void verifyDepthExportClamp() { require(legacy.request.context.pixel->depthExportEnable && legacy.request.context.pixel->depthExportMin == 0.0f && legacy.request.context.pixel->depthExportMax == 1.0f && legacy.request.target.largePixelFunctionLdsBuffer && legacy.request.target.pixelFunctionStorageDwords == 2048u, - "depth export: a version " + std::to_string(legacyVersion) + " depth exporting request did not read with default bounds and its target tail"); + legacyVersion == 46u ? "depth export: a version 46 depth exporting request did not read with default bounds and its target tail" + : "depth export: a version 47 depth exporting request did not read with default bounds and its target tail"); } for (const bool depthExport : {false, true}) { auto dithered = request; From bd13b15189b744baf880ac66806f7debeea35cf5 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 10:43:56 +0100 Subject: [PATCH 468/702] test(libSceAudiodec): cover invalid codec types and handle bounds in GuestAudiodec --- core/libs/tests/GuestAudiodec.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestAudiodec.cpp b/core/libs/tests/GuestAudiodec.cpp index ed04f8ee5d..6461dd9bf4 100644 --- a/core/libs/tests/GuestAudiodec.cpp +++ b/core/libs/tests/GuestAudiodec.cpp @@ -369,8 +369,16 @@ void TestAt9() { } int main() { + Require(sceAudiodecInitLibrary(0) == errorInvalidType); Require(sceAudiodecInitLibrary(4) == errorInvalidType); Require(sceAudiodecTermLibrary(0) == errorInvalidType); + Require(sceAudiodecTermLibrary(4) == errorInvalidType); + Require(sceAudiodecDeleteDecoder(0) == errorInvalidHandle); + Require(sceAudiodecDeleteDecoder(-1) == errorInvalidHandle); + Require(sceAudiodecClearContext(0) == errorInvalidHandle); + Require(sceAudiodecClearContext(-1) == errorInvalidHandle); + Require(sceAudiodecDecode(0, nullptr) == errorInvalidHandle); + Require(sceAudiodecDecode(-1, nullptr) == errorInvalidHandle); AudiodecParamMp3 mp3{sizeof(mp3), 1}; AudiodecMp3Info mp3Info{}; @@ -379,6 +387,7 @@ int main() { Require(sceAudiodecCreateDecoder(&ctrl, 2) == errorArg); for (std::uint32_t type = 1; type <= 3; ++type) Require(sceAudiodecInitLibrary(type) == 0); Require(sceAudiodecCreateDecoder(nullptr, 2) == errorInvalidCtrlPointer); + Require(sceAudiodecCreateDecoder(&ctrl, 0) == errorInvalidType); Require(sceAudiodecCreateDecoder(&ctrl, 5) == errorInvalidType); mp3.ui_size = 12; Require(sceAudiodecCreateDecoder(&ctrl, 2) == errorInvalidParamSize); From 3d17309724b626eecbf3d974f565b32c43609e12 Mon Sep 17 00:00:00 2001 From: Hedi Trigui <hedi.trigui.13@gmail.com> Date: Tue, 27 Oct 2026 10:46:33 +0100 Subject: [PATCH 469/702] feat(libc): implement reallocarray --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/FileAndHeap.cpp | 8 ++++++++ core/libs/tests/ApplicationHeap.cpp | 9 +++++++++ docs/dev/debt/libc.md | 2 +- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index cf0eb2256a..686c654b11 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -253,6 +253,7 @@ void* APS5_VABI malloc_nid_postfix(size_t size); void APS5_VABI free_nid_postfix(void* ptr); void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize); void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize); +void* APS5_VABI reallocarray_nid_postfix(void* ptr, size_t nmemb, size_t size); void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size); void* APS5_VABI aligned_alloc_nid_postfix(size_t alignment, size_t size); void* APS5_VABI reallocalign_nid_postfix(void* ptr, size_t size, size_t alignment); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 364ba40805..31c897f4b2 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -649,6 +649,14 @@ void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { return result; } +void* APS5_VABI reallocarray_nid_postfix(void* ptr, size_t nmemb, size_t size) { + if (size != 0 && nmemb > std::numeric_limits<size_t>::max() / size) { + errno = 12; + return nullptr; + } + return realloc_nid_postfix(ptr, nmemb * size); +} + void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { if (!ApplicationHeapDefault_nid_no_patch()) { if (!validAlignment(alignment)) { diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 27894e4131..62f136df73 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -333,6 +333,15 @@ int main(int argc, char** argv) { empty = ApplicationHeapReallocate_nid_no_patch(nullptr, 0); require(empty != nullptr); ApplicationHeapFree_nid_no_patch(empty); + pointer = static_cast<unsigned char*>(reallocarray_nid_postfix(nullptr, 4, 10)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 40); + std::memset(pointer, 0x5a, 40); + pointer = static_cast<unsigned char*>(reallocarray_nid_postfix(pointer, 5, 20)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 100); + for (unsigned i = 0; i < 40; ++i) require(pointer[i] == 0x5a); + *__error_nid_postfix() = 0; + require(reallocarray_nid_postfix(pointer, 2, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12); + ApplicationHeapFree_nid_no_patch(pointer); for (std::size_t alignment : {16, 64, 4096}) { auto* aligned = ApplicationHeapAlign_nid_no_patch(alignment, 37); require(reinterpret_cast<std::uintptr_t>(aligned) % alignment == 0); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c3a5564270..a6555a1f2f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -35,7 +35,7 @@ - [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. - [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` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. -- [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. +- [malloc, calloc, realloc, reallocarray, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. - [reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - `reallocalign(ptr, 0, alignment)` for a block without that alignment throws, where the console's heap in a payload process on PS5 firmware 13.60 returned a different block with that alignment and 0 usable bytes, `errno` untouched (a 64-aligned block given 256); whether it frees the old block is not known. There, as here, `reallocalign(ptr, 0, 16)` on a 256-aligned block returned it unchanged, `reallocalign(NULL, 0, 256)` a 256-aligned block with 0 usable bytes, and alignment 3 a moved block that kept its bytes - [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`, as a payload process on a PS5 (firmware 13.60) does with the default heap (null and 0, size 0, 2^40 with `errno` 22 and the block freed, `SIZE_MAX` with `errno` untouched): size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does From c70008b8534f1cfe61fc8bbf3ebaa2fbca3ed212 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:24:15 +0100 Subject: [PATCH 470/702] fix(relinker): reject ET_EXEC executable input RelinkerPipeline::Relink accepted ET_EXEC input, and every Linux mode placed it at base + p_vaddr: at 0x400000 by default, at the --linux-fixed-base address, or at a load bias as ET_DYN with --lazy-binding, so its absolute references that carry no relocation kept pointing at the link addresses. The gABI chapter on program loading says the segments of an executable file must reside at the virtual addresses used to build it. ET_DYN and the SCE dynamic types 0xfe10 and 0xfe18, the types WindowsLoadImage already accepts as position independent, may be placed at a base. Relink now rejects ET_EXEC on every target before the allowlist, which accepts ET_DYN, 0xfe10 and 0xfe18. ET_EXEC input now exits 2 without writing output; outputs for the other accepted types are unchanged. --- core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp | 6 ++++-- core/relinker/relinker/tests/test_elf_header.py | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index ceda75412d..a824f929b1 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -66,9 +66,11 @@ RelinkResult RelinkerPipeline::Relink() { throw RelinkerException("ET_NONE inputs do not specify an ELF object type"); if (inputType == kEtCore) throw RelinkerException("ET_CORE core dumps are not executable images"); - if (inputType != ET_EXEC && inputType != ET_DYN && inputType != kEtSceDynExec && inputType != kEtSceDynamic) { + if (inputType == ET_EXEC) + throw RelinkerException("ET_EXEC inputs are linked at fixed addresses; conversion requires a position-independent image", 0x10); + if (inputType != ET_DYN && inputType != kEtSceDynExec && inputType != kEtSceDynamic) { std::ostringstream message; - message << "Unsupported ELF object type 0x" << std::hex << inputType << ": expected ET_EXEC, ET_DYN, 0xfe10 or 0xfe18"; + message << "Unsupported ELF object type 0x" << std::hex << inputType << ": expected ET_DYN, 0xfe10 or 0xfe18"; throw RelinkerException(message.str(), 0x10); } diff --git a/core/relinker/relinker/tests/test_elf_header.py b/core/relinker/relinker/tests/test_elf_header.py index dcc7530a07..34fe4f3c1c 100644 --- a/core/relinker/relinker/tests/test_elf_header.py +++ b/core/relinker/relinker/tests/test_elf_header.py @@ -78,12 +78,11 @@ def object_type_cases(relinker, directory): (1, "ET_REL relocatable inputs are not supported"), (4, "ET_CORE core dumps are not executable images"), *((value, f"Unsupported ELF object type 0x{value:x}") for value in (5, 0x1234, 0xFE00, 0xFE01, 0xFE04, 0xFFFF)), - (2, None), (3, None), (0xFE10, None), (0xFE18, None)) + (2, "ET_EXEC inputs are linked at fixed addresses"), + (3, None), (0xFE10, None), (0xFE18, None)) for mode, flags in enumerate(MODES): for filter_level in (0, 1, 2): for object_type, error in cases: - if object_type == 2 and ("--windows" in flags or "--macos" in flags): - error = "This target requires a position-independent ELF image" case = Path(directory) / f"type-{mode}-{filter_level}-{object_type}" case.mkdir() source = case / "input.elf" From f7874ba21ecdb5541cbf0136fa602a6598599feb Mon Sep 17 00:00:00 2001 From: blawesom <blaplane@gmail.com> Date: Mon, 26 Oct 2026 12:20:40 +0100 Subject: [PATCH 471/702] build: add configure_host_test_executable for host-only test executables A test executable that links no guest library and is built for MinGW fails to link when one of its objects has an exception landing pad: the guest libc provides __gxx_personality_v0, and host-only links do not see it. configure_host_test_executable(<target>) centralises the fix already spelled out by hand in core/libs: native SEH unwind tables and a static runtime. Migrated the two targets whose blocks matched it exactly (patched_libraries_tests, guest_kernel_sys_threads_tests); their MinGW build is unchanged by construction. Documented the rule in CONVENTIONS. Unblocks none; guards the same class as the CI check in #11343. --- CMakeLists.txt | 7 +++++++ core/libs/CMakeLists.txt | 10 ++-------- docs/dev/CONVENTIONS.md | 5 +++++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7db1bb7432..a0e3677ed9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,13 @@ function(add_test_executable target) endif() endfunction() +function(configure_host_test_executable target) + if(MINGW) + target_compile_options(${target} PRIVATE -fasynchronous-unwind-tables) + target_link_options(${target} PRIVATE -static -static-libgcc -static-libstdc++) + endif() +endfunction() + function(add_asan_test_options test options) if(ANYPS5_ENABLE_ASAN) set_property(TEST ${test} APPEND PROPERTY ENVIRONMENT_MODIFICATION "ASAN_OPTIONS=string_append::${options}") diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index b1de7b4e4a..3003579b8c 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -369,10 +369,7 @@ if(BUILD_TESTING) add_test(NAME nid_pe_references COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_pe_references.py $<TARGET_FILE:nid_patcher>) endif() - if(MINGW) - target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables) - target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++) - endif() + configure_host_test_executable(patched_libraries_tests) add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR}) if(MINGW) get_filename_component(patchedLibrariesRuntimeDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) @@ -452,10 +449,7 @@ if(BUILD_TESTING) ${PATCHED_DIR}/libkernel_sys.prx ${PATCHED_DIR}/libkernel.prx ${LIBC_PATCHED}) add_dependencies(guest_kernel_sys_threads_tests guest_kernel_sys_threads_libraries) configure_windows_unwind(guest_kernel_sys_threads_tests) - if(MINGW) - target_compile_options(guest_kernel_sys_threads_tests PRIVATE -fasynchronous-unwind-tables) - target_link_options(guest_kernel_sys_threads_tests PRIVATE -static -static-libgcc -static-libstdc++) - endif() + configure_host_test_executable(guest_kernel_sys_threads_tests) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test(NAME guest_kernel_sys_threads_main_late_load COMMAND guest_kernel_sys_threads_tests ${UNPATCHED_DIR} unpatched main_late_load) add_test(NAME guest_kernel_sys_threads_main_late_load_patched COMMAND guest_kernel_sys_threads_tests ${PATCHED_DIR} patched main_late_load) diff --git a/docs/dev/CONVENTIONS.md b/docs/dev/CONVENTIONS.md index fdf0b83768..0351d53131 100644 --- a/docs/dev/CONVENTIONS.md +++ b/docs/dev/CONVENTIONS.md @@ -26,3 +26,8 @@ The write watch can leave guest pages read-only between collections: every watch - Ordinary CPU stores from host C++ code need no bracket: the write watch records them (through a fault where the page is protected). - A GPU import of guest memory uses `HostImportAccess`, not `HostWrite`. - Third-party code that writes guest memory on its own (driver threads, library worker threads) can't be bracketed. List it in that library's debt file. +### Host-only test executables + +A test executable that links no guest library and is built for the host on MinGW must call `configure_host_test_executable(<target>)` (root `CMakeLists.txt`) after `add_test_executable`. It supplies the two flags such an executable needs to link when one of its objects has an exception landing pad, and nothing else. + +The guest `libc` provides `__gxx_personality_v0`; a host-only link doesn't get it. On a DWARF-exception MinGW toolchain an object with a landing pad then fails to link. Whether a given target needs the fix depends on its code, so it can't be found by reading the CMake files: call the helper on every host-only test executable. It is not needed for test executables that link a guest library, and not on non-MinGW hosts. From bceec11fb1f43853ead82caf28150755ed870b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <adriadam10@gmail.com> Date: Tue, 27 Oct 2026 10:51:35 +0100 Subject: [PATCH 472/702] docs: limit the host-only test executable rule to targets under core/libs Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/CONVENTIONS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/CONVENTIONS.md b/docs/dev/CONVENTIONS.md index 0351d53131..e71460b547 100644 --- a/docs/dev/CONVENTIONS.md +++ b/docs/dev/CONVENTIONS.md @@ -28,6 +28,6 @@ The write watch can leave guest pages read-only between collections: every watch - Third-party code that writes guest memory on its own (driver threads, library worker threads) can't be bracketed. List it in that library's debt file. ### Host-only test executables -A test executable that links no guest library and is built for the host on MinGW must call `configure_host_test_executable(<target>)` (root `CMakeLists.txt`) after `add_test_executable`. It supplies the two flags such an executable needs to link when one of its objects has an exception landing pad, and nothing else. +A host-only test executable (one that links no guest library) whose sources inherit the `-fno-asynchronous-unwind-tables` policy of `core/libs/CMakeLists.txt`, and that is built for MinGW, must call `configure_host_test_executable(<target>)` (root `CMakeLists.txt`) after `add_test_executable`. It supplies the two flags such an executable needs to link when one of its objects has an exception landing pad, and nothing else. -The guest `libc` provides `__gxx_personality_v0`; a host-only link doesn't get it. On a DWARF-exception MinGW toolchain an object with a landing pad then fails to link. Whether a given target needs the fix depends on its code, so it can't be found by reading the CMake files: call the helper on every host-only test executable. It is not needed for test executables that link a guest library, and not on non-MinGW hosts. +The guest `libc` provides `__gxx_personality_v0`; a host-only link doesn't get it. On a DWARF-exception MinGW toolchain an object with a landing pad then fails to link. Test executables defined in the root `CMakeLists.txt` do not inherit that policy, so they do not need the helper; nor do test executables that link a guest library, or non-MinGW hosts. From 1f8adf12f5edda07eca6dcfb1c058cfd37888adc Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:52:03 +0100 Subject: [PATCH 473/702] docs(libc): keep the Sun and FreeBSD notices of e_sinh.c and k_exp.c above sinh Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Math.cpp | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9c76c67f37..1d90761fd7 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1031,6 +1031,32 @@ void APS5_VABI sincosf_nid_postfix(float x, float* sinp, float* cosp) { double APS5_VABI sin_nid_postfix(double x) { return std::sin(x); } double APS5_VABI cos_nid_postfix(double x) { return std::cos(x); } +/*- + * Copyright (c) 2011 David Schultz <das@FreeBSD.ORG> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +/* Adapted from FreeBSD release/11.4.0 lib/msun/src/k_exp.c: https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/k_exp.c */ static double SinhLdexpExpHalf(double x) { constexpr std::uint32_t k = 1799; constexpr double kln2 = 1246.97177782734161156; @@ -1042,6 +1068,17 @@ static double SinhLdexpExpHalf(double x) { const double scaled = std::bit_cast<double>((bits & 0xffffffffull) | (static_cast<std::uint64_t>(scaledHigh) << 32)); return scaled * std::bit_cast<double>(static_cast<std::uint64_t>(0x3ff + expt) << 52); } +/* + * ==================================================== + * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. + * + * Developed at SunSoft, a Sun Microsystems, Inc. business. + * Permission to use, copy, modify, and distribute this + * software is freely granted, provided that this notice + * is preserved. + * ==================================================== + */ +/* Adapted from FreeBSD release/11.4.0 lib/msun/src/e_sinh.c: https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_sinh.c */ double APS5_VABI sinh_nid_postfix(double x) { const double one = 1.0; const double shuge = 1.0e307; From e984b14b3f12c58106efecdfe5be27c137829e1e Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:24:56 +0200 Subject: [PATCH 474/702] fix(libkernel): sysconf answers the names a PS5 payload process answers --- .../System/src/SystemConfiguration.cpp | 39 ++++++++++++++++++- core/libs/tests/GuestSystemConfiguration.cpp | 37 ++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index b6c40105c9..944d6d7288 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -191,6 +191,32 @@ bool IsUnansweredMib(const int* name, std::uint32_t nameLength) { return (name[0] == 1 && (name[1] == 6 || name[1] == 7 || name[1] == 12 || name[1] == 27 || name[1] == 32)) || (name[0] == 6 && (name[1] == 4 || name[1] == 11)); } +struct SysconfConstant { + int name; + std::int64_t value; +}; + +constexpr SysconfConstant kSysconfConstants[] = { + {6, 1}, {7, -1}, {8, 200112}, {9, 99}, {10, 2048}, {11, 99}, {12, 1000}, {13, 0}, {14, 32}, + {15, 2048}, {16, 255}, {17, 199212}, {18, 200112}, {19, -1}, {20, 1}, {21, -1}, {22, 200112}, + {23, -1}, {24, -1}, {25, 200112}, {28, 200112}, {29, 200112}, {30, -1}, {31, 200112}, {32, 200112}, + {33, 200112}, {34, -1}, {35, 200112}, {36, 200112}, {37, 200112}, {38, 200112}, {39, 200112}, + {40, -1}, {41, 200112}, {42, 16}, {43, 1024}, {44, 0}, {45, 2147483647}, {46, -1}, {48, 62}, + {49, 30}, {50, 2147483647}, {51, 128}, {52, 32}, {59, -1}, {60, -1}, {61, -1}, {62, -1}, {63, -1}, + {64, -1}, {65, 200112}, {66, 200112}, {67, -1}, {68, 200112}, {74, 200112}, {75, 64}, {76, 200112}, + {77, 1}, {78, 1}, {79, 200112}, {80, 200112}, {81, -1}, {82, 200112}, {83, 200112}, {84, 200112}, + {85, 4}, {86, 256}, {87, 200112}, {88, 200112}, {89, 200112}, {90, -1}, {91, -1}, {93, 16384}, + {94, -1}, {95, 200112}, {96, 200112}, {97, -1}, {101, 255}, {102, -1}, {103, -1}, {104, -1}, + {105, 1}, {106, -1}, {107, 64}, {108, -1}, {109, -1}, {110, -1}, {111, -1}, {112, -1}, {113, -1}, + {114, -1}, {115, -1}, {118, 200112}, {119, 200112}, {120, 32}, {122, 8}, +}; + +const SysconfConstant* FindSysconfConstant(int name) { + const auto* end = std::end(kSysconfConstants); + const auto* entry = std::lower_bound(std::begin(kSysconfConstants), end, name, [](const SysconfConstant& constant, int key) { return constant.name < key; }); + return entry != end && entry->name == name ? entry : nullptr; +} + bool IsUnansweredName(const char* name) { static constexpr const char* unanswered[] = { "hw.byteorder", @@ -285,9 +311,18 @@ std::int64_t APS5_VABI sysconf_nid_postfix(int name) { #endif break; } - default: - *__error_nid_postfix() = 22; + case 27: + *__error_nid_postfix() = 2; return -1; + default: { + const SysconfConstant* constant = FindSysconfConstant(name); + if (!constant) { + *__error_nid_postfix() = 22; + return -1; + } + *__error_nid_postfix() = saved; + return constant->value; + } } if (result <= 0) { *__error_nid_postfix() = 5; return -1; } *__error_nid_postfix() = saved; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 9c19aa6e91..b18122bef0 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -1011,6 +1011,42 @@ static void CheckUnknownRootsAndTopLevelNames() { Require(sysctlbyname_nid_postfix(tooLong.c_str(), &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); } +static void CheckSysconfMeasuredNames() { + struct Entry { + int name; + std::int64_t value; + }; + static constexpr Entry constants[] = { + {6, 1}, {7, -1}, {8, 200112}, {9, 99}, {10, 2048}, {11, 99}, {12, 1000}, {13, 0}, {14, 32}, + {15, 2048}, {16, 255}, {17, 199212}, {18, 200112}, {19, -1}, {20, 1}, {21, -1}, {22, 200112}, + {23, -1}, {24, -1}, {25, 200112}, {28, 200112}, {29, 200112}, {30, -1}, {31, 200112}, {32, 200112}, + {33, 200112}, {34, -1}, {35, 200112}, {36, 200112}, {37, 200112}, {38, 200112}, {39, 200112}, + {40, -1}, {41, 200112}, {42, 16}, {43, 1024}, {44, 0}, {45, 2147483647}, {46, -1}, {48, 62}, + {49, 30}, {50, 2147483647}, {51, 128}, {52, 32}, {59, -1}, {60, -1}, {61, -1}, {62, -1}, {63, -1}, + {64, -1}, {65, 200112}, {66, 200112}, {67, -1}, {68, 200112}, {74, 200112}, {75, 64}, {76, 200112}, + {77, 1}, {78, 1}, {79, 200112}, {80, 200112}, {81, -1}, {82, 200112}, {83, 200112}, {84, 200112}, + {85, 4}, {86, 256}, {87, 200112}, {88, 200112}, {89, 200112}, {90, -1}, {91, -1}, {93, 16384}, + {94, -1}, {95, 200112}, {96, 200112}, {97, -1}, {101, 255}, {102, -1}, {103, -1}, {104, -1}, + {105, 1}, {106, -1}, {107, 64}, {108, -1}, {109, -1}, {110, -1}, {111, -1}, {112, -1}, {113, -1}, + {114, -1}, {115, -1}, {118, 200112}, {119, 200112}, {120, 32}, {122, 8}, + {1, 262144}, {3, 128}, {4, 1023}, {47, 16384}, {56, 1024}, {72, 255}, {73, 33}, + }; + for (const Entry& entry : constants) { + *__error_nid_postfix() = 77; + Require(sysconf_nid_postfix(entry.name) == entry.value); + Require(*__error_nid_postfix() == 77); + } + *__error_nid_postfix() = 77; + Require(sysconf_nid_postfix(27) == -1); + Require(*__error_nid_postfix() == 2); + static constexpr int rejected[] = {-1, 0, 53, 54, 55, 69, 70, 71, 92, 98, 99, 100, 116, 117, 123, 124, 125, 126, 127, 128, 129, 130, 150, 200, 201, 1000}; + for (int name : rejected) { + *__error_nid_postfix() = 77; + Require(sysconf_nid_postfix(name) == -1); + Require(*__error_nid_postfix() == 22); + } +} + int main() { CheckUnknownRootsAndTopLevelNames(); CheckProcessorCountSysctl(); @@ -1040,6 +1076,7 @@ int main() { CheckEnvSysctl(); CheckPseudoRandomMibSysctl(); CheckNameToMib(); + CheckSysconfMeasuredNames(); *__error_nid_postfix() = 13; Require(sysconf_nid_postfix(1) == 262144); Require(sysconf_nid_postfix(3) == 128); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0fe8091311..bb8a79d72b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -26,7 +26,7 @@ - [setlogin](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel_sys) keeps one name for the process, where FreeBSD 11.4's [`sys_setlogin`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_prot.c#L2138-L2161) sets it for the session, and does not check `PRIV_PROC_SETLOGIN`, which a jailed process fails with `EPERM`; the console's answer is not known. As in FreeBSD, a null name fails with `EFAULT` and a name of 33 bytes or more (`MAXLOGNAME` with the NUL) with `EINVAL`, and nothing is stored. Once a name is stored, `getlogin_r` returns it instead of the host account name described for `getlogin_r`, and ahead of the `LoginNameForTest_nid_no_patch` seam. The name is copied from the guest with the same 33-byte bound as `copyinstr`, so a name with no NUL in its first 33 bytes fails with `EINVAL` without reading past them. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. Measured on 13.60: the same holds for values 1, 0x1000 and 2^64-1, this API still reports "13.590.001" while the Prospero variant reports "13.600.007", and a null pointer returns 0 without a fault or an `errno` change. Other firmware versions remain unverified. - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. 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 is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` maps the name to {1, 50} and `sysctl` {1, 50} answers as the name does; a write through the MIB was not measured and throws -- [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it; the console's value is unverified and no `times` reports ticks at that rate. CPython in PS5MC (PPSA99420) requires it >= 1. `_SC_ARG_MAX` returns 262144, `_SC_CHILD_MAX` the `RLIMIT_NPROC` limit via `getrlimit`, `_SC_NGROUPS_MAX` 1023 (`KERN_NGROUPS`), `_SC_OPEN_MAX` the `RLIMIT_NOFILE` limit via `getrlimit`, `_SC_STREAM_MAX` that limit bounded by `SHRT_MAX`, `_SC_HOST_NAME_MAX` 255, `_SC_LOGIN_NAME_MAX` 33, and `_SC_IOV_MAX` 1024 (`UIO_MAXIOV`). Other names fail with `EINVAL` +- [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` report the host's processor count and memory in 16 KiB pages, where a payload process on a PS5 (firmware 12.70) read 16, 16 and 139888. `_SC_CHILD_MAX`, `_SC_OPEN_MAX` and `_SC_STREAM_MAX` follow the guest `RLIMIT_NPROC` and `RLIMIT_NOFILE`; following a changed `RLIMIT_NPROC` was not measured, and the 32767 bound on `_SC_STREAM_MAX` was not tried above 13952. `_SC_CLK_TCK` returns the 128 that process read, but no `times` reports ticks at that rate; CPython in PS5MC (PPSA99420) needs >= 1. `_SC_CPUSET_SIZE` (122) returns 8, as that process read, and whether it follows the processor count is unknown. A failed `getrlimit` or a host count of 0 gives -1 with `EIO`, which was not seen on the console. What a title process gets was not measured - [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for `hw.physmem` and `hw.realmem` (a read, a size query and a null length) and for {`CTL_HW`, 5} and {`CTL_HW`, 12} (a read and a size query); what a title process gets was not measured. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) - [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer, and a write throws. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for it, for a read, a size query and a null length; what a title process gets was not measured. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up - [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 From fe80227fce02056fefbcbacaaa9aaec4cf823517 Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:02:24 +0000 Subject: [PATCH 475/702] fix(tools): reject unmodeled save-temps modes in syntax checks --- docs/dev/TechnicalDebt.md | 1 + tools/check_compile.py | 2 ++ tools/tests/test_check_compile.py | 31 +++++++++++++++---------------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index cbfc2f0b45..47831bf914 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -29,3 +29,4 @@ Throughout the project, every function at every stage either **does exactly what - `--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. - [Quick syntax checks](../../tools/check_compile.py) do not support Windows-style response-file quoting. GNU response files reject empty options, byte order marks, quoted CRLF, Windows text-mode EOF markers and escapes whose meanings differ between GCC and Clang, including backslashes in single quotes, most backslashes in double quotes and escaped line endings. Supply expanded compile arguments for those forms; the Ninja fallback uses `ninja -t compdb -x`. - [Quick syntax checks](../../tools/check_compile.py) do not model every compiler option's operand count. A preprocessing mode following an unrecognized option without a joined value is rejected rather than risk removing that option's operand. Compiler wrappers with their own option grammar are not covered. Forwarded frontend actions (`-Xclang`, `-Xpreprocessor` and their joined forms) are rejected. Comma-forwarded options (`-Wp,`) allow only recognized macro/include operands; response-file tokens are rejected even as operands, and other options are rejected because unmodeled actions or output operands could bypass syntax analysis or write outputs. See the [Clang option grammar](https://clang.llvm.org/docs/ClangCommandLineReference.html). +- [Quick syntax checks](../../tools/check_compile.py) reject `-save-temps=` and `--save-temps=` values other than `cwd` and `obj` instead of passing them to the compiler: an unknown value fails on GCC, while newer Clang builds take any value as an output prefix and write preprocessed files even under `-fsyntax-only` (measured on Apple clang 21), so a passed-through mode either fails the check on one compiler or silently writes outputs on the other. diff --git a/tools/check_compile.py b/tools/check_compile.py index b103ce948a..d404edd2fa 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -272,6 +272,8 @@ def syntax_arguments(arguments, directory, source): trimmed.append(forwarded) index += 1 continue + if not positional and argument.startswith(("-save-temps=", "--save-temps=")) and argument not in DROP: + raise ValueError(f"unsupported saved-temporary mode in {argument}") if not positional and (argument in (*DROP, *PREPROCESSING) or argument.startswith(("-MT", "-MF", "-MQ", "-MJ"))): index += 1 continue diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index e40969615c..87a807aa40 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -607,23 +607,22 @@ class CheckCompileOutputTests(CheckCompileFixture): self.assertEqual(path.read_bytes(), content, str(path)) self.assertEqual(set(self.build.iterdir()), set(preserved)) - def test_rejected_saved_temporary_mode_is_not_hidden(self): - compiler = shutil.which("gcc") or shutil.which("clang") - if compiler is None: - self.skipTest("needs a C compiler") + def test_unmodeled_saved_temporary_modes_are_rejected_before_the_compiler_runs(self): source = self.source("unit.c") - original = subprocess.run([compiler, "-save-temps=invalid", "-fsyntax-only", str(source)], - cwd=self.build, capture_output=True, text=True) - if original.returncode == 0: - self.skipTest("compiler accepts unknown saved-temporary modes with -fsyntax-only") - self.assertIn("save-temps", original.stderr) - self.manifest([{"directory": str(self.build), "file": str(source), - "arguments": [compiler, "-save-temps=invalid", "-c", str(source), - "-o", str(self.build / "unit.o")]}]) - result = self.tool("unit.c") - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn("save-temps", result.stderr) - self.assertFalse((self.build / "unit.o").exists()) + invoked = self.root / "invoked" + script = self.compiler("import sys\nfrom pathlib import Path\nPath(sys.argv[1]).touch()\n") + for option in ("-save-temps=invalid", "-save-temps=diag", "--save-temps=diag", + "-save-temps=CWD", "-save-temps="): + with self.subTest(option=option): + self.manifest([{"directory": str(self.build), "file": str(source), + "arguments": [sys.executable, str(script), str(invoked), option, + "-c", str(source), "-o", str(self.build / "unit.o")]}]) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn(f"unsupported saved-temporary mode in {option}", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse(invoked.exists(), result.stdout + result.stderr) + self.assertFalse((self.build / "unit.o").exists(), result.stdout + result.stderr) class CheckCompileResponseTests(CheckCompileFixture): From f89e32170ce3bc8fc785148b5236a1c81adaa6d2 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Sat, 24 Oct 2026 03:53:07 -0400 Subject: [PATCH 476/702] fix(libc): preserve expm1 scaling association under directed rounding --- core/libs/prx/libc/src/Math.cpp | 2 +- core/libs/tests/GuestMath.cpp | 39 +++++++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index eecd5815e7..6989cc5dae 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -742,7 +742,7 @@ static double Expm1(double x) { } double y; if (k <= -2 || k > 56) { - y = 1.0 - (e - x); + y = 1.0 + (x - e); if (k == 1024) { y = y * 2.0 * 0x1p1023; } else { diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index f8e0f9da06..eff6881db4 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -917,6 +917,44 @@ static void CheckNextafterEnvironment(T (APS5_VABI *next)(T, T), UInt sign, UInt _mm_setcsr(savedControl); } +static void CheckExpm1ScalingRounding() { + const RestoreFloatingPointState saved; + const unsigned savedCsr = _mm_getcsr(); + const struct { + std::uint64_t input; + std::uint64_t result[4]; + } cases[] = { + {0x407ff33f4d52628cull, {0x6e06c8c735268691ull, 0x6e06c8c73526868full, 0x6e06c8c735268691ull, 0x6e06c8c735268690ull}}, + {0x407eff5467ccdb38ull, {0x6ca6e270fe0a802eull, 0x6ca6e270fe0a802cull, 0x6ca6e270fe0a802eull, 0x6ca6e270fe0a802cull}}, + }; + const int rounding[] = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + double (APS5_VABI * volatile call)(double) = expm1_nid_postfix; + for (unsigned mode = 0; mode != 4; ++mode) { + for (unsigned denorm = 0; denorm != 4; ++denorm) { + const unsigned control = 0x1f80u | (mode << 13u) | ((denorm & 1u) ? 0x40u : 0u) | ((denorm & 2u) ? 0x8000u : 0u); + for (std::uint16_t sticky : {0u, 5u, 32u}) { + for (const auto& item : cases) { + Require(std::fesetenv(FE_DFL_ENV) == 0 && std::fesetround(rounding[mode]) == 0); + Require(fesetexceptflag_nid_postfix(&sticky, 0x3f) == 0); + _mm_setcsr(control | sticky); + GuestFloatingPointEnvironment before; + Require(fegetenv_nid_postfix(&before) == 0 && (before.status & 0x3fu) == sticky); + errno = 1234; + const auto result = std::bit_cast<std::uint64_t>(call(std::bit_cast<double>(item.input))); + const int error = errno; + GuestFloatingPointEnvironment after; + Require(fegetenv_nid_postfix(&after) == 0); + if (result != item.result[mode] || after.mxcsr != (control | sticky | 0x20u) || (after.status & 0x3fu) != sticky || after.control != before.control || error != 1234) { + std::fprintf(stderr, "expm1 scaling: input=%016llx mode=%u denorm=%u sticky=%u bits=%016llx csr=%x x87=%x errno=%d\n", static_cast<unsigned long long>(item.input), mode, denorm, sticky, static_cast<unsigned long long>(result), after.mxcsr, after.status, error); + Require(false); + } + } + } + } + } + _mm_setcsr(savedCsr); +} + 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}, @@ -2210,6 +2248,7 @@ int main() { CheckRoundingNaNConsoleBits(); CheckNextafterEnvironment(nextafterf_nid_postfix, 0x80000000u, 0x3f800000u, 0x00800000u, 0x7f7fffffu, 0x7f800000u); CheckNextafterEnvironment(nextafter_nid_postfix, std::uint64_t{0x8000000000000000ull}, std::uint64_t{0x3ff0000000000000ull}, std::uint64_t{0x0010000000000000ull}, std::uint64_t{0x7fefffffffffffffull}, std::uint64_t{0x7ff0000000000000ull}); + CheckExpm1ScalingRounding(); CheckAsinl(); Require(scalbn_nid_postfix(0.75, -2) == 0.1875); Require(scalbnf_nid_postfix(0.75f, 4) == 12.f); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c3a5564270..6eec93dc78 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -124,7 +124,7 @@ - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [coshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows FreeBSD msun's [`e_coshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_coshf.c), using the `expm1f` and `expf` paths already inlined for `sinhf` below; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 500,554 and 500,614 of 20 million inputs in the #9707 survey. The exact system `libSceLibcInternal` behavior and the FMA-contracted `libc.prx` results remain unverified - [powf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `powf` of the `libc.prx` that titles ship: FreeBSD msun's [`e_powf.c`](https://github.com/freebsd/freebsd-src/blob/release/9.0.0/lib/msun/src/e_powf.c) as of 9.0.0, which has 11.4's arithmetic but no `1**y` shortcut, so a NaN argument returns its NaN (`powf(1, NaN)` is NaN, not 1), and `(±1)**±inf` returns `y - y`, a NaN. With two NaN arguments the first one's NaN is returned, quieted. `errno` is set to `EDOM` in the two `(x - x) / (x - x)` branches (negative finite `x`, non-integer `y`), and to `ERANGE` only in the final overflow and underflow returns; the `|y| > 2^27` shortcut and the zero and infinity cases leave it unchanged. Floating-point exceptions are not compared. On 100M sampled argument pairs it gives the same bits as the copies bundled with PPSA07923 and PPSA05916, with the same `errno`; the FMA-contracted copy of PPSA13934 differs by 1 ulp on 26,405 of them. Pairs outside the sample are unverified. -- [expm1](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_expm1.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_expm1.c) with `huge` read at run time, and sets `ERANGE` only when a finite argument overflows to infinity, as the titles' `libc.prx` does. It gives the same bits and `errno` as the `expm1` bundled with PPSA07923 and PPSA05916 on 320 million random and 47,222 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 43,433 of the random ones. +- [expm1](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_expm1.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_expm1.c) with `huge` read at run time, and sets `ERANGE` only when a finite argument overflows to infinity, as the titles' `libc.prx` does. It gives the same bits and `errno` as the `expm1` bundled with PPSA07923 and PPSA05916 on 320 million random and 47,222 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 43,433 of the random ones. Scaling uses `1 + (x - e)`, matching the supplied PPSA03766 and PPSA07923 modules under directed rounding, rather than `1 - (e - x)`. - [log1p](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_log1p.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_log1p.c). As the titles' `libc.prx` does, it sets `EDOM` at msun's NaN return (arguments below -1, `-inf` and negative NaNs) and leaves `errno` alone for `log1p(-1)`, where it returns `-inf`. It gives the same bits, NaN payloads included, and `errno` as the `log1p` bundled with PPSA07923 and PPSA05916 on 320 million random and 68,216 structured arguments on a Windows host. The FMA-contracted build bundled with PPSA13934 differs on 73,682 of the random ones. - [nextafter and nextafterf](../../../core/libs/prx/libc/src/Math.cpp) (libc) have no physical-console or unmasked-exception validation. A Linux CPU probe compares result bits, `errno` and masked floating-point flags with the unchanged math code in FAR: Changing Tides PPSA03766 bundled `libc.prx` (`nextafter` 0x51b30, `nextafterf` 0x51c80, `_Feraise` 0x11da0): 16,864 calls across both precisions, four rounding modes and all DAZ/FTZ combinations, not exhaustive inputs. Earlier Windows sampling of eleven bundled libraries compares only bits and `errno`; exception behavior outside the FAR module remains unverified. - [tanh](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_tanh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_tanh.c) over this file's msun `Expm1`, with `tiny` read at run time; it sets no `errno`, as the titles' `libc.prx` does not. It gives the same bits as the `tanh` bundled with PPSA07923 and PPSA05916 on 16 million random and 68,211 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 1,054 of them. From 04ea7b1575c80fd6ddaf1e4e4c74a47f60a7d89e Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Mon, 26 Oct 2026 22:43:01 -0400 Subject: [PATCH 477/702] test(agc): preserve descriptors written after a memoized depth collect --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 ++++++ .../execution/DepthPlanePartialStore.cpp | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d935ce5043..4194d8208c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3272,6 +3272,13 @@ if(BUILD_TESTING) endif() endforeach() endforeach() + add_test(NAME agc_driver_depth_plane_cpu_write_after_memo COMMAND agc_driver_depth_plane_partial_store_tests --cpu-write-after-memo) + set_tests_properties(agc_driver_depth_plane_cpu_write_after_memo PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_depth_plane_cpu_write_after_memo PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_driver_depth_plane_partial_store_unimported COMMAND agc_driver_depth_plane_partial_store_tests) add_test(NAME agc_driver_depth_plane_partial_store_buffer_copy COMMAND agc_driver_depth_plane_partial_store_tests --buffer-copy) foreach(partialStoreTest agc_driver_depth_plane_partial_store_unimported agc_driver_depth_plane_partial_store_buffer_copy) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp index 6f8c3c8bdc..0486d4ba8c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp @@ -24,9 +24,11 @@ #include <cstdint> #include <cstdio> #include <cstdlib> +#include <cstring> #include <iostream> #include <span> #include <string> +#include <string_view> #include <utility> #include <vector> @@ -140,6 +142,27 @@ int main(int argc, char** argv) { const auto draw = [&](const Surface& surface, const DrawOptions& options) { return Draw(*device, shaders, surface, options, PlaneExtent, Colors); }; const DrawOptions writeHalf{.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}; + if (argc == 2 && std::string_view(argv[1]) == "--cpu-write-after-memo") { + if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(base, PlaneBytes)) return VulkanTestSkipped; + Fill(plane, 1.0f); + Require(draw(depth, writeHalf) == PlaneTexels, "the memo regression depth draw did not cover the plane"); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::GuestMemory::CollectWrites(base, PlaneBytes); + const auto vertexAddress = base + 2u * Block + 256u; + const std::array<std::uint32_t, 4> descriptor{static_cast<std::uint32_t>(vertexAddress), static_cast<std::uint32_t>((vertexAddress >> 32u) & 0xffffu) | (32u << 16u), 4u, 0x0004dfacu}; + auto* table = reinterpret_cast<std::byte*>(plane) + 4u * Block + 4096u; + std::memcpy(table, descriptor.data(), sizeof(descriptor)); + const auto stores = AgcDriver::Graphics::FlushDepthPlanes(base + 4u * Block + 128u, 192u); + device->WaitIdle(); + Require(std::memcmp(table, descriptor.data(), sizeof(descriptor)) == 0, "a depth flush overwrote CPU descriptors written after a memoized collect"); + Require(stores.empty(), "a depth flush stored the unit repurposed by the CPU"); + Require(!ReadStart(*device, base).empty(), "preserving the CPU descriptors dropped an untouched depth unit"); + RequireUnit(plane, 0, 0.5f, UnitTexels, "the untouched unit retains its GPU depth results"); + Require(std::memcmp(table, descriptor.data(), sizeof(descriptor)) == 0, "storing an untouched depth unit changed the CPU descriptors"); + std::puts("depth plane CPU descriptors after memoized collect tests passed"); + return 0; + } + Fill(plane, 1.0f); Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 did not cover the plane"); Require(AgcDriver::Graphics::DepthPlanesStale(), "the draw writing depth left the plane unregistered"); From 748661795896eca6fd54dbae3a7aad3de45ac859 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Mon, 26 Oct 2026 22:51:48 -0400 Subject: [PATCH 478/702] fix(agc): collect fresh CPU writes before depth plane stores --- core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 6834e77f84..c462bba63f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -2218,6 +2218,7 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint6 surface->ClearStale(); continue; } + GuestMemory::CollectWritesUncached(plane, static_cast<std::size_t>(planeBytes)); surface->SyncFromMemory(resource); auto units = scope == DepthPlaneScope::Whole ? surface->staleUnits : surface->StaleUnitsIn(planeBytes, address, end); if (anyUnit(units)) { From 24e88cd20a34972fc4906e71054a34746189f9e1 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 23:29:59 +1300 Subject: [PATCH 479/702] fix(libkernel): publish scattered AMM pool pages exactly once Move the pool bookkeeping into AmmPool.hpp. A map from a fragmented pool now plans its pages without changing the pool, stages the page and reference records in their own maps, maps the runs, and only then splits the pool and moves the records in with no allocation, so a failed map leaves the pool and the records as they were. Adding a page inserts its reference record first and undoes it if the page record throws. --- .../prx/libkernel/Apr/include/AmmPool.hpp | 111 ++++++++++++++++++ core/libs/prx/libkernel/Apr/src/Apr.cpp | 73 ++---------- 2 files changed, 120 insertions(+), 64 deletions(-) create mode 100644 core/libs/prx/libkernel/Apr/include/AmmPool.hpp diff --git a/core/libs/prx/libkernel/Apr/include/AmmPool.hpp b/core/libs/prx/libkernel/Apr/include/AmmPool.hpp new file mode 100644 index 0000000000..aaaba740d2 --- /dev/null +++ b/core/libs/prx/libkernel/Apr/include/AmmPool.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include <algorithm> +#include <cstdint> +#include <iterator> +#include <map> +#include <stdexcept> +#include <string> +#include <vector> + +namespace AmmPool { + +constexpr std::uint64_t PageBytes = 0x4000; + +struct Page { + std::uint64_t physical; + std::int32_t type; + std::int32_t protection; + bool pooled; +}; + +struct Records { + std::map<std::uint64_t, std::uint64_t> pool; + std::map<std::uintptr_t, Page> pages; + std::map<std::uint64_t, std::uint32_t> pooledUses; +}; + +inline void ReturnPages(Records& records, std::uint64_t offset, std::uint64_t bytes) { + auto next = records.pool.lower_bound(offset); + if (next != records.pool.end() && next->first == offset + bytes) { + bytes += next->second; + next = records.pool.erase(next); + } + if (next != records.pool.begin()) { + const auto previous = std::prev(next); + if (previous->first + previous->second == offset) { + previous->second += bytes; + return; + } + } + records.pool.emplace(offset, bytes); +} + +inline bool TryTakeRun(Records& records, std::uint64_t bytes, std::uint64_t& offset) { + for (auto it = records.pool.begin(); it != records.pool.end(); ++it) { + if (it->second < bytes) continue; + offset = it->first; + const auto remaining = it->second - bytes; + records.pool.erase(it); + if (remaining != 0) records.pool.emplace(offset + bytes, remaining); + return true; + } + return false; +} + +inline void AddPage(Records& records, std::uint64_t address, const Page& page) { + if (!page.pooled) { + records.pages[address] = page; + return; + } + const auto [uses, inserted] = records.pooledUses.try_emplace(page.physical, 0); + try { + records.pages[address] = page; + } catch (...) { + if (inserted) records.pooledUses.erase(uses); + throw; + } + ++uses->second; +} + +template <class MapRun> +void MapScattered(Records& records, std::uint64_t address, std::uint64_t bytes, std::int32_t type, std::int32_t protection, MapRun&& mapRun) { + std::uint64_t free = 0; + for (const auto& [offset, size] : records.pool) free += size; + if (free < bytes) throw std::runtime_error("AMM: no free run of " + std::to_string(bytes) + " bytes in the direct memory given to the mapper"); + std::vector<Page> pages; + pages.reserve(bytes / PageBytes); + std::vector<std::map<std::uint64_t, std::uint64_t>::iterator> runs; + runs.reserve(records.pool.size()); + std::uint64_t remainderOffset = 0; + std::uint64_t remainderBytes = 0; + std::uint64_t remaining = bytes; + for (auto it = records.pool.begin(); remaining != 0; ++it) { + const auto take = std::min(it->second, remaining); + for (std::uint64_t page = 0; page < take; page += PageBytes) pages.push_back({it->first + page, type, protection, true}); + runs.push_back(it); + if (take < it->second) { + remainderOffset = it->first + take; + remainderBytes = it->second - take; + } + remaining -= take; + } + std::map<std::uintptr_t, Page> stagedPages; + std::map<std::uint64_t, std::uint32_t> stagedUses; + for (std::size_t index = 0; index < pages.size(); ++index) { + stagedPages.emplace(address + index * PageBytes, pages[index]); + stagedUses.emplace(pages[index].physical, 1); + } + for (std::size_t first = 0; first < pages.size();) { + std::size_t last = first + 1; + while (last < pages.size() && pages[last].physical == pages[last - 1].physical + PageBytes) ++last; + mapRun(address + first * PageBytes, pages[first].physical, (last - first) * PageBytes); + first = last; + } + if (remainderBytes != 0) records.pool.emplace(remainderOffset, remainderBytes); + for (const auto run : runs) records.pool.erase(run); + records.pages.merge(stagedPages); + records.pooledUses.merge(stagedUses); +} + +} diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 4b266b0f68..caee13089f 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -12,6 +12,7 @@ #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" +#include "prx/libkernel/Apr/include/AmmPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" @@ -281,19 +282,13 @@ constexpr std::uint64_t AmmRangeAlignment = 64ull << 30; constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; -struct AmmPage { - std::uint64_t physical; - std::int32_t type; - std::int32_t protection; - bool pooled; -}; +static_assert(AmmPool::PageBytes == PS5_PAGE_SIZE); -struct AmmState { +using AmmPage = AmmPool::Page; + +struct AmmState : AmmPool::Records { std::mutex lock; std::uintptr_t base = 0; - std::map<std::uint64_t, std::uint64_t> pool; - std::map<std::uintptr_t, AmmPage> pages; - std::map<std::uint64_t, std::uint32_t> pooledUses; std::set<std::uintptr_t> prt; }; @@ -331,31 +326,11 @@ int _ammHostProtection(std::int32_t protection) { } void _returnPoolPages(AmmState& state, std::uint64_t offset, std::uint64_t bytes) { - auto next = state.pool.lower_bound(offset); - if (next != state.pool.end() && next->first == offset + bytes) { - bytes += next->second; - next = state.pool.erase(next); - } - if (next != state.pool.begin()) { - const auto previous = std::prev(next); - if (previous->first + previous->second == offset) { - previous->second += bytes; - return; - } - } - state.pool.emplace(offset, bytes); + AmmPool::ReturnPages(state, offset, bytes); } bool _tryTakePoolPages(AmmState& state, std::uint64_t bytes, std::uint64_t& offset) { - for (auto it = state.pool.begin(); it != state.pool.end(); ++it) { - if (it->second < bytes) continue; - offset = it->first; - const auto remaining = it->second - bytes; - state.pool.erase(it); - if (remaining != 0) state.pool.emplace(offset + bytes, remaining); - return true; - } - return false; + return AmmPool::TryTakeRun(state, bytes, offset); } std::uint64_t _takePoolPages(AmmState& state, std::uint64_t bytes) { @@ -364,27 +339,8 @@ std::uint64_t _takePoolPages(AmmState& state, std::uint64_t bytes) { return offset; } -std::vector<AmmPage> _takeScatteredPages(AmmState& state, std::uint64_t bytes, std::int32_t type, std::int32_t protection) { - std::uint64_t free = 0; - for (const auto& [offset, size] : state.pool) free += size; - if (free < bytes) throw std::runtime_error("AMM: no free run of " + std::to_string(bytes) + " bytes in the direct memory given to the mapper"); - std::vector<AmmPage> pages; - pages.reserve(bytes / PS5_PAGE_SIZE); - while (pages.size() * PS5_PAGE_SIZE < bytes) { - auto it = state.pool.begin(); - const auto take = std::min(it->second, bytes - pages.size() * PS5_PAGE_SIZE); - const auto offset = it->first; - const auto remaining = it->second - take; - state.pool.erase(it); - if (remaining != 0) state.pool.emplace(offset + take, remaining); - for (std::uint64_t page = 0; page < take; page += PS5_PAGE_SIZE) pages.push_back({offset + page, type, protection, true}); - } - return pages; -} - void _addPage(AmmState& state, std::uint64_t address, const AmmPage& page) { - state.pages[address] = page; - if (page.pooled) ++state.pooledUses[page.physical]; + AmmPool::AddPage(state, address, page); } void _releasePage(AmmState& state, const AmmPage& page) { @@ -537,20 +493,9 @@ void _ammMap(const Apr::AmmMapCommand& command, bool direct) { _clearPrt(state, command.address, command.size); std::uint64_t physical = direct ? command.directOffset : 0; if (!direct && !_tryTakePoolPages(state, command.size, physical)) { - const auto pages = _takeScatteredPages(state, command.size, command.type, command.protection); - std::size_t mapped = 0; try { - for (std::size_t first = 0; first < pages.size();) { - std::size_t last = first + 1; - while (last < pages.size() && pages[last].physical == pages[last - 1].physical + PS5_PAGE_SIZE) ++last; - _mapRun(command.address + first * PS5_PAGE_SIZE, pages[first].physical, (last - first) * PS5_PAGE_SIZE, command.protection); - for (std::size_t index = first; index < last; ++index) _addPage(state, command.address + index * PS5_PAGE_SIZE, pages[index]); - mapped = last; - first = last; - } + AmmPool::MapScattered(state, command.address, command.size, command.type, command.protection, [&](std::uint64_t address, std::uint64_t physicalRun, std::uint64_t size) { _mapRun(address, physicalRun, size, command.protection); }); } catch (...) { - _forgetPages(state, command.address, command.size); - for (std::size_t index = mapped; index < pages.size(); ++index) _returnPoolPages(state, pages[index].physical, PS5_PAGE_SIZE); _reserve(command.address, command.size); throw; } From 77c978e31870e530338cb2684329358153098be8 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 23:29:59 +1300 Subject: [PATCH 480/702] test(libkernel): fail every allocation of a scattered AMM map in turn Fail each allocation of a map from a fragmented pool, a map whose host call fails after each run, and a page insertion, and check that the pool and the page records are unchanged after every failure and that a map of one page more than the pool holds is still refused. --- core/libs/prx/libkernel/CMakeLists.txt | 5 + core/libs/tests/GuestAmmPool.cpp | 247 +++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 core/libs/tests/GuestAmmPool.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 974df3b88c..2c0cc9124f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1173,6 +1173,11 @@ if(BUILD_TESTING) set_tests_properties(guest_exception_handler PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") endif() + add_test_executable(guest_amm_pool_tests ${LIBS_INCLUDE_DIR}/tests/GuestAmmPool.cpp) + target_include_directories(guest_amm_pool_tests PRIVATE ${LIBS_INCLUDE_DIR}) + add_test(NAME guest_amm_pool COMMAND guest_amm_pool_tests) + set_tests_properties(guest_amm_pool PROPERTIES TIMEOUT 30) + add_test_executable(guest_raise_exception_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseException.cpp) target_include_directories(guest_raise_exception_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_raise_exception_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestAmmPool.cpp b/core/libs/tests/GuestAmmPool.cpp new file mode 100644 index 0000000000..f2f6d59b08 --- /dev/null +++ b/core/libs/tests/GuestAmmPool.cpp @@ -0,0 +1,247 @@ +#include "prx/libkernel/Apr/include/AmmPool.hpp" +#include <atomic> +#include <cstdio> +#include <cstdlib> +#include <exception> +#include <functional> +#include <new> +#include <string> + +static std::atomic<long> failingAllocation{-1}; + +void* operator new(std::size_t size) { + if (failingAllocation.load(std::memory_order_relaxed) >= 0 && failingAllocation.fetch_sub(1, std::memory_order_relaxed) == 0) throw std::bad_alloc(); + if (void* memory = std::malloc(size == 0 ? 1 : size)) return memory; + throw std::bad_alloc(); +} + +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } + +static void RequireAt(bool value, int line) { + if (value) return; + std::fprintf(stderr, "GuestAmmPool.cpp:%d: requirement failed\n", line); + std::abort(); +} + +#define Require(value) RequireAt((value), __LINE__) + +namespace { + +constexpr std::uint64_t page = AmmPool::PageBytes; +constexpr std::uint64_t address = 0x7000000000; + +bool Equal(const AmmPool::Records& a, const AmmPool::Records& b) { + const auto samePage = [](const AmmPool::Page& x, const AmmPool::Page& y) { return x.physical == y.physical && x.type == y.type && x.protection == y.protection && x.pooled == y.pooled; }; + if (a.pool != b.pool || a.pooledUses != b.pooledUses || a.pages.size() != b.pages.size()) return false; + for (auto x = a.pages.begin(), y = b.pages.begin(); x != a.pages.end(); ++x, ++y) { + if (x->first != y->first || !samePage(x->second, y->second)) return false; + } + return true; +} + +std::uint64_t FreeBytes(const AmmPool::Records& records) { + std::uint64_t free = 0; + for (const auto& [offset, size] : records.pool) free += size; + return free; +} + +void RequireConsistent(const AmmPool::Records& records) { + std::map<std::uint64_t, std::uint32_t> uses; + for (const auto& [at, entry] : records.pages) { + if (entry.pooled) ++uses[entry.physical]; + } + Require(uses == records.pooledUses); + std::uint64_t previousEnd = 0; + bool first = true; + for (const auto& [offset, size] : records.pool) { + Require(size != 0 && offset % page == 0 && size % page == 0); + Require(first || offset > previousEnd); + previousEnd = offset + size; + first = false; + } + for (const auto& [physical, count] : uses) { + for (const auto& [offset, size] : records.pool) Require(physical >= offset + size || physical + page <= offset); + } +} + +AmmPool::Records Fragmented() { + AmmPool::Records records; + records.pool.emplace(0, page); + records.pool.emplace(2 * page, page); + records.pool.emplace(4 * page, page); + records.pages.emplace(address - 2 * page, AmmPool::Page{page, 0, 3, true}); + records.pooledUses.emplace(page, 1); + records.pages.emplace(address - page, AmmPool::Page{3 * page, 0, 3, true}); + records.pooledUses.emplace(3 * page, 1); + return records; +} + +void Map(AmmPool::Records& records, std::uint64_t bytes, const std::function<void(std::uint64_t, std::uint64_t, std::uint64_t)>& mapRun) { + AmmPool::MapScattered(records, address, bytes, 1, 3, mapRun); +} + +void TestScatteredMap() { + auto records = Fragmented(); + std::map<std::uint64_t, std::uint64_t> runs; + Map(records, 3 * page, [&](std::uint64_t at, std::uint64_t physical, std::uint64_t size) { runs.emplace(at, physical); Require(size == page); }); + Require(records.pool.empty()); + Require(runs.size() == 3 && runs.at(address) == 0 && runs.at(address + page) == 2 * page && runs.at(address + 2 * page) == 4 * page); + for (std::uint64_t index = 0; index < 3; ++index) { + const auto found = records.pages.find(address + index * page); + Require(found != records.pages.end() && found->second.physical == 2 * index * page && found->second.pooled && found->second.type == 1 && found->second.protection == 3); + Require(records.pooledUses.at(2 * index * page) == 1); + } + RequireConsistent(records); +} + +void TestSplitRun() { + AmmPool::Records records; + records.pool.emplace(0, page); + records.pool.emplace(2 * page, 3 * page); + std::vector<std::pair<std::uint64_t, std::uint64_t>> runs; + Map(records, 3 * page, [&](std::uint64_t at, std::uint64_t physical, std::uint64_t size) { runs.emplace_back(physical, size); }); + Require(runs.size() == 2 && runs[0] == std::make_pair(std::uint64_t{0}, page) && runs[1] == std::make_pair(2 * page, 2 * page)); + Require(records.pool.size() == 1 && records.pool.at(4 * page) == page); + Require(records.pages.size() == 3 && records.pooledUses.size() == 3); + RequireConsistent(records); +} + +void TestRefusals() { + auto records = Fragmented(); + const auto before = records; + bool refused = false; + try { + Map(records, 4 * page, [&](std::uint64_t, std::uint64_t, std::uint64_t) { Require(false); }); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).starts_with("AMM: no free run of "); + } + Require(refused && Equal(records, before)); +} + +void TestHostMapFailsAfterEachRun() { + for (int failAt = 0; failAt < 3; ++failAt) { + auto records = Fragmented(); + const auto before = records; + int mapped = 0; + bool failed = false; + try { + Map(records, 3 * page, [&](std::uint64_t, std::uint64_t, std::uint64_t) { + if (mapped++ == failAt) throw std::runtime_error("AMM: map failed"); + }); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed && Equal(records, before)); + RequireConsistent(records); + Map(records, 3 * page, [&](std::uint64_t, std::uint64_t, std::uint64_t) {}); + Require(FreeBytes(records) == 0); + RequireConsistent(records); + } +} + +void TestEveryAllocationFails() { + long failures = 0; + for (long allocation = 0;; ++allocation) { + Require(allocation < 1000); + auto records = Fragmented(); + const auto before = records; + bool failed = false; + failingAllocation.store(allocation); + try { + Map(records, 3 * page, [&](std::uint64_t, std::uint64_t, std::uint64_t) {}); + } catch (const std::bad_alloc&) { + failed = true; + } + failingAllocation.store(-1); + if (!failed) { + Require(records.pool.empty() && records.pages.size() == 5 && records.pooledUses.size() == 5); + RequireConsistent(records); + break; + } + ++failures; + Require(Equal(records, before)); + RequireConsistent(records); + Map(records, 3 * page, [&](std::uint64_t, std::uint64_t, std::uint64_t) {}); + Require(FreeBytes(records) == 0); + RequireConsistent(records); + } + Require(failures >= 8); +} + +void TestEveryAllocationFailsWithASplit() { + long failures = 0; + for (long allocation = 0;; ++allocation) { + Require(allocation < 1000); + AmmPool::Records records; + records.pool.emplace(0, page); + records.pool.emplace(2 * page, 3 * page); + const auto before = records; + bool failed = false; + failingAllocation.store(allocation); + try { + Map(records, 3 * page, [&](std::uint64_t, std::uint64_t, std::uint64_t) {}); + } catch (const std::bad_alloc&) { + failed = true; + } + failingAllocation.store(-1); + if (!failed) { + Require(records.pool.size() == 1 && records.pool.at(4 * page) == page && records.pages.size() == 3); + RequireConsistent(records); + break; + } + ++failures; + Require(Equal(records, before)); + RequireConsistent(records); + Map(records, 4 * page, [&](std::uint64_t, std::uint64_t, std::uint64_t) {}); + Require(FreeBytes(records) == 0); + } + Require(failures >= 8); +} + +void TestAddPageFailures() { + long failures = 0; + for (long allocation = 0;; ++allocation) { + Require(allocation < 100); + AmmPool::Records records; + records.pages.emplace(address, AmmPool::Page{0, 0, 3, true}); + records.pooledUses.emplace(0, 1); + const auto before = records; + bool failed = false; + failingAllocation.store(allocation); + try { + AmmPool::AddPage(records, address + page, AmmPool::Page{page, 0, 3, true}); + } catch (const std::bad_alloc&) { + failed = true; + } + failingAllocation.store(-1); + if (!failed) { + Require(records.pages.size() == 2 && records.pooledUses.at(page) == 1); + RequireConsistent(records); + break; + } + ++failures; + Require(Equal(records, before)); + RequireConsistent(records); + } + Require(failures >= 1); +} + +} + +int main() { + try { + TestScatteredMap(); + TestSplitRun(); + TestRefusals(); + TestHostMapFailsAfterEachRun(); + TestEveryAllocationFails(); + TestEveryAllocationFailsWithASplit(); + TestAddPageFailures(); + std::puts("AMM scattered pool pages published exactly once"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From e3b5192cb47eaae26590f5210d7337417b6fe74d Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 23:41:47 +1300 Subject: [PATCH 481/702] fix(libkernel): map PRT-allocated AMM pages through the pool staging _ammAllocatePrt took pool pages one at a time and mapped and recorded them run by run, so a failure on run k lost the pool pages of runs k to n. It now maps the unbacked part through AmmPool::MapScattered and returns the range to the reservation if that throws, and the unused _takePoolPages is gone. --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 12 ++--------- core/libs/tests/GuestAmmPool.cpp | 27 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index caee13089f..2a69fbf0cf 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -333,12 +333,6 @@ bool _tryTakePoolPages(AmmState& state, std::uint64_t bytes, std::uint64_t& offs return AmmPool::TryTakeRun(state, bytes, offset); } -std::uint64_t _takePoolPages(AmmState& state, std::uint64_t bytes) { - std::uint64_t offset = 0; - if (!_tryTakePoolPages(state, bytes, offset)) throw std::runtime_error("AMM: no free run of " + std::to_string(bytes) + " bytes in the direct memory given to the mapper"); - return offset; -} - void _addPage(AmmState& state, std::uint64_t address, const AmmPage& page) { AmmPool::AddPage(state, address, page); } @@ -471,14 +465,12 @@ void _ammAllocatePrt(const Apr::AmmProtectCommand& command) { } if (DoMprotect(reinterpret_cast<void*>(address), bytes, _ammHostProtection(command.protection)) != 0) throw std::runtime_error("AMM: protection change failed"); } else { - std::vector<AmmPage> pages; try { - for (std::uint64_t page = 0; page < bytes; page += PS5_PAGE_SIZE) pages.push_back({_takePoolPages(state, PS5_PAGE_SIZE), command.type, command.protection, true}); + AmmPool::MapScattered(state, address, bytes, command.type, command.protection, [&](std::uint64_t at, std::uint64_t physical, std::uint64_t size) { _mapRun(at, physical, size, command.protection); }); } catch (...) { - for (const auto& page : pages) _returnPoolPages(state, page.physical, PS5_PAGE_SIZE); + _reserve(address, bytes); throw; } - _mapPages(state, address, pages); } offset = end; } diff --git a/core/libs/tests/GuestAmmPool.cpp b/core/libs/tests/GuestAmmPool.cpp index f2f6d59b08..78d96378ae 100644 --- a/core/libs/tests/GuestAmmPool.cpp +++ b/core/libs/tests/GuestAmmPool.cpp @@ -199,6 +199,32 @@ void TestEveryAllocationFailsWithASplit() { Require(failures >= 8); } +void TestPrtAllocationShape() { + for (int failAt = 0; failAt < 3; ++failAt) { + AmmPool::Records records; + records.pool.emplace(0, 2 * page); + records.pool.emplace(3 * page, page); + records.pool.emplace(5 * page, 2 * page); + const auto before = records; + int mapped = 0; + bool failed = false; + try { + Map(records, 5 * page, [&](std::uint64_t, std::uint64_t, std::uint64_t) { + if (mapped++ == failAt) throw std::runtime_error("AMM: map failed"); + }); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed && Equal(records, before)); + RequireConsistent(records); + std::vector<std::uint64_t> sizes; + Map(records, 5 * page, [&](std::uint64_t, std::uint64_t, std::uint64_t size) { sizes.push_back(size); }); + Require(sizes.size() == 3 && sizes[0] == 2 * page && sizes[1] == page && sizes[2] == 2 * page); + Require(FreeBytes(records) == 0 && records.pages.size() == 5); + RequireConsistent(records); + } +} + void TestAddPageFailures() { long failures = 0; for (long allocation = 0;; ++allocation) { @@ -237,6 +263,7 @@ int main() { TestHostMapFailsAfterEachRun(); TestEveryAllocationFails(); TestEveryAllocationFailsWithASplit(); + TestPrtAllocationShape(); TestAddPageFailures(); std::puts("AMM scattered pool pages published exactly once"); return 0; From 79fc99774b4b82d892535a73b96ce94bc7c4caec Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 23:45:02 +1300 Subject: [PATCH 482/702] fix(libkernel): zero the PRT range again when allocating pages for it fails Unbacked pages of a PRT range are anonymous zero pages, so a failed allocation puts those back with _zeroPrt instead of leaving the range reserved and unreadable. --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 2a69fbf0cf..0ba7bbafeb 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -468,7 +468,7 @@ void _ammAllocatePrt(const Apr::AmmProtectCommand& command) { try { AmmPool::MapScattered(state, address, bytes, command.type, command.protection, [&](std::uint64_t at, std::uint64_t physical, std::uint64_t size) { _mapRun(at, physical, size, command.protection); }); } catch (...) { - _reserve(address, bytes); + _zeroPrt(address, bytes); throw; } } From 74ad6b1a5f90dde63a9c8932c9f621e4aac99edf Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 27 Oct 2026 23:45:02 +1300 Subject: [PATCH 483/702] test(libkernel): read zeros from a PRT range after a failed page allocation Allocate more pages for a PRT range than the pool holds and check the range still reads zero, then allocate one page, write it, and check the next allocation is refused the same way. --- core/libs/tests/GuestAmpr.cpp | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index f2394f5687..962f01490e 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -1172,6 +1172,54 @@ void TestAmmMapsScatteredPoolPages() { Require(exhausts(empty)); } +void TestAmmPrtAllocationFailureKeepsZeros() { + 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); + const auto exhausts = [](Recorder& recorder) { + try { + SubmitAmm(recorder); + } catch (const std::runtime_error& error) { + return std::string(error.what()).starts_with("AMM: no free run of "); + } + return false; + }; + const auto drained = start + 24576 * page; + for (std::uint64_t index = 0;; ++index) { + Require(index < 4096); + Recorder drain; + Require(sceAmprAmmCommandBufferMap(&drain.buffer, drained + index * page, page, 0, cpuReadWrite) == 0); + if (exhausts(drain)) break; + } + std::int64_t pool = -1; + Require(sceAmprAmmGiveDirectMemory(0, static_cast<std::int64_t>(sceKernelGetDirectMemorySize()), page, page, 1, &pool) == 0); + + const std::uint64_t prt = start + 28672 * page; + Recorder reserve; + Require(sceAmprAmmCommandBufferMapAsPrt(&reserve.buffer, prt, 4 * page) == 0); + SubmitAmm(reserve); + Require(At(prt) == 0 && At(prt + 3 * page) == 0); + + Recorder tooMany; + Require(sceAmprAmmCommandBufferAllocatePaForPrt(&tooMany.buffer, prt + page, 2 * page, 0, cpuReadWrite) == 0); + Require(exhausts(tooMany)); + Require(Protection(prt + page) == 0x10 && At(prt) == 0 && At(prt + page) == 0 && At(prt + 2 * page) == 0 && At(prt + 3 * page) == 0); + + Recorder one; + Require(sceAmprAmmCommandBufferAllocatePaForPrt(&one.buffer, prt + page, page, 0, cpuReadWrite) == 0); + SubmitAmm(one); + Require(Protection(prt + page) == 0x03); + At(prt + page) = 0x61; + Require(At(prt + page) == 0x61 && At(prt) == 0 && At(prt + 2 * page) == 0); + + Recorder none; + Require(sceAmprAmmCommandBufferAllocatePaForPrt(&none.buffer, prt + 2 * page, page, 0, cpuReadWrite) == 0); + Require(exhausts(none)); + Require(At(prt + 2 * page) == 0 && At(prt + page) == 0x61); +} + void TestAmmMapFailureKeepsPoolOwnership() { std::uint64_t start = 0; std::uint64_t end = 0; @@ -1518,6 +1566,7 @@ int main() { TestAmm(); TestAmmRemapAndProtect(); TestAmmPrt(); + TestAmmPrtAllocationFailureKeepsZeros(); TestKernelWriteCommands(); return 0; } From be358dc3f7fbca637ea1a43714aeb87d9379e6a4 Mon Sep 17 00:00:00 2001 From: B-Howell <116104752+B-Howell@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:33:50 -0400 Subject: [PATCH 484/702] feat(libSceVideoOut): allow selecting the initial display --- .../libs/prx/libSceVideoOut/src/DisplayWindow.cpp | 15 +++++++++++++-- docs/dev/ENVIRONMENT.md | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index ad6f310991..8383a60f1d 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -25,6 +25,16 @@ void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("DisplayWindow: ") + reason); } +int initialDisplayIndex() { + const auto value = std::getenv("ANYPS5_DISPLAY_INDEX"); + if (value == nullptr) return 0; + char* end = nullptr; + const auto index = std::strtol(value, &end, 10); + const auto count = SDL_GetNumVideoDisplays(); + require(end != value && *end == '\0' && index >= 0 && index < count, "ANYPS5_DISPLAY_INDEX is not a connected display index"); + return static_cast<int>(index); +} + void applyAppIcon(SDL_Window* window) { if (!HasAppIcon_nid_postfix()) return; const auto data = GetAppIconData_nid_postfix(); @@ -54,7 +64,8 @@ void DisplayWindow::Ensure(std::uint32_t sourceWidth, std::uint32_t sourceHeight void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { SDL_Rect usable{}; - require(SDL_GetDisplayUsableBounds(0, &usable) == 0, SDL_GetError()); + const auto displayIndex = initialDisplayIndex(); + require(SDL_GetDisplayUsableBounds(displayIndex, &usable) == 0, SDL_GetError()); require(DisplayWindowInitialSizePercent > 0 && DisplayWindowInitialSizePercent <= 100, "initial window size percent must be between 1 and 100"); require(usable.w > 0 && usable.h > 0, "usable display extent must be positive"); const auto boundsWidth = static_cast<std::uint32_t>(static_cast<std::uint64_t>(usable.w) * DisplayWindowInitialSizePercent / 100); @@ -65,7 +76,7 @@ void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight AgcDriverLockVulkanLoader_nid_postfix(); const char* fullscreen = std::getenv("ANYPS5_FULLSCREEN"); const Uint32 fullscreenFlag = fullscreen != nullptr && *fullscreen != '\0' && std::strcmp(fullscreen, "0") != 0 ? static_cast<Uint32>(SDL_WINDOW_FULLSCREEN_DESKTOP) : 0u; - window = SDL_CreateWindow(title.value, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast<int>(initialSize.width), static_cast<int>(initialSize.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | fullscreenFlag); + window = SDL_CreateWindow(title.value, SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex), SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex), static_cast<int>(initialSize.width), static_cast<int>(initialSize.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | fullscreenFlag); AgcDriverUnlockVulkanLoader_nid_postfix(); require(window != nullptr, SDL_GetError()); SDL_SetWindowMinimumSize(window, static_cast<int>(DisplayWindowMinimumWidth), static_cast<int>(DisplayWindowMinimumHeight)); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 8ad35349c0..347dbd10ab 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -557,6 +557,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| | `ANYPS5_FULLSCREEN` | presence | `core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp` | | opens the display window fullscreen at the desktop resolution when set to a non-empty value other than 0; F11 still switches back | +| `ANYPS5_DISPLAY_INDEX` | integer | `core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp` | 0 | SDL display index used for the initial window size and centered position; invalid or unavailable indices fail window creation; selection does not guarantee the window will not activate | | `ANYPS5_INPUT_CONFIG` | string | `core/libs/prx/libSceVideoOut/src/InputMapping.cpp` | | path of the input mapping file that binds host keys and mouse to guest pad actions | | `ANYPS5_REQUIRE_DISPLAY` | presence | `core/libs/prx/libSceVideoOut/tests/Flip.cpp` | | makes video-out tests fail instead of skipping when no display or Vulkan device is available | | `APS5_NO_PAD_INPUT` | presence | `core/libs/prx/libSceVideoOut/src/PadInput.cpp` | | ignores keyboard and mouse input so a measurement run is not advanced by a stray press | From bb5a62ce1177efd79806ebeb275e9d3e3646e158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <adriadam10@gmail.com> Date: Tue, 27 Oct 2026 11:50:09 +0100 Subject: [PATCH 485/702] docs(environment): sort ANYPS5_DISPLAY_INDEX within libSceVideoOut --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 347dbd10ab..ba733b8929 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -556,8 +556,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `ANYPS5_FULLSCREEN` | presence | `core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp` | | opens the display window fullscreen at the desktop resolution when set to a non-empty value other than 0; F11 still switches back | | `ANYPS5_DISPLAY_INDEX` | integer | `core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp` | 0 | SDL display index used for the initial window size and centered position; invalid or unavailable indices fail window creation; selection does not guarantee the window will not activate | +| `ANYPS5_FULLSCREEN` | presence | `core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp` | | opens the display window fullscreen at the desktop resolution when set to a non-empty value other than 0; F11 still switches back | | `ANYPS5_INPUT_CONFIG` | string | `core/libs/prx/libSceVideoOut/src/InputMapping.cpp` | | path of the input mapping file that binds host keys and mouse to guest pad actions | | `ANYPS5_REQUIRE_DISPLAY` | presence | `core/libs/prx/libSceVideoOut/tests/Flip.cpp` | | makes video-out tests fail instead of skipping when no display or Vulkan device is available | | `APS5_NO_PAD_INPUT` | presence | `core/libs/prx/libSceVideoOut/src/PadInput.cpp` | | ignores keyboard and mouse input so a measurement run is not advanced by a stray press | From 724cabcffdcd9010d90e928e532bb321d0e97c1a Mon Sep 17 00:00:00 2001 From: frankea <frankea@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:52:31 -0400 Subject: [PATCH 486/702] fix(shader): evaluate a control point masked out of the packed hull register --- .../tests/TessellationLowering.cpp | 35 +++++++++++++++++++ .../tests/execution/Tessellation.cpp | 11 ++++++ .../Optimization/src/TessellationLowering.cpp | 7 +++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp index b978b4c018..7aaf23fe9f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp @@ -133,6 +133,10 @@ struct Hull { void readRing(IrValue& offset, IrValue& predicate) { auto& loaded = builder.Emit(IrOpcode::LoadBufferU32, IrType::U32, {&ringResource(), &builder.Constant(0u), &offset, &offChip(), &predicate}, MemoryFlags{RingMemory, RingReadPc}); static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&loaded})); + } + + IrValue& packed() { + return builder.BitwiseOr(builder.ShiftLeftLogical(invocation(), builder.Constant(8u)), relativePatch()); } IrValue& merge(std::uint32_t firstMultiple, std::uint32_t secondMultiple) { @@ -341,6 +345,14 @@ bool LowersUnderComplementedBallot(Conjunction conjunction) { return LoweredAt(hull.program, address); } +bool OwnControlPoint(const IrValue* address, std::uint32_t stride) { + if (address == nullptr || address->Opcode() != IrOpcode::IAdd32 || !address->Argument(1)->Resolve()->HasImmediate() || address->Argument(1)->Resolve()->ImmediateU32() != 0u) return false; + const IrValue* scaled = address->Argument(0)->Resolve(); + if (scaled->Opcode() != IrOpcode::IMul32 || scaled->Argument(1)->Resolve()->ImmediateU32() != stride) return false; + const IrValue* invocation = scaled->Argument(0)->Resolve(); + return invocation->Opcode() == IrOpcode::GetBuiltin && invocation->Argument(0)->Resolve()->ImmediateU32() == static_cast<std::uint32_t>(StageInputKind::InvocationId); +} + bool run(const char* name, const std::function<bool()>& body) { bool passed = false; try { @@ -548,6 +560,29 @@ int main() { passed &= run("a hull read of another control point's output at an immediate address past the last control point reads the last control point", [] { return EmittedControlPoint(IrShaderStage::TessellationControl, TessellationAttribute::ControlOutput, InputControlPoints * 16u) == InputControlPoints - 1u; + }); + + passed &= run("a hull read at its control point masked out of the packed control point and relative patch reads its own input control point", [] { + Hull hull; + auto& masked = hull.builder.BitwiseAnd(hull.packed(), hull.builder.Constant(0x1f00u)); + hull.read(hull.inPatch(hull.builder.ShiftRightLogical(masked, hull.builder.Constant(3u)))); + const Outcome outcome = Lower(hull.program); + if (!outcome.lowered) std::fprintf(stderr, "refused: %s\n", outcome.reason.c_str()); + return outcome.lowered && OwnControlPoint(outcome.emitted, LocalStride); + }); + + passed &= run("a mask that drops the low bit of the packed control point is refused", [] { + Hull hull; + auto& masked = hull.builder.BitwiseAnd(hull.packed(), hull.builder.Constant(0x1e00u)); + hull.read(hull.inPatch(hull.builder.ShiftRightLogical(masked, hull.builder.Constant(3u)))); + return RefusedBecause(hull.program, "not affine"); + }); + + passed &= run("a mask with a gap below the packed control point is refused", [] { + Hull hull; + auto& masked = hull.builder.BitwiseAnd(hull.packed(), hull.builder.Constant(0x1f0fu)); + hull.read(hull.inPatch(hull.builder.ShiftRightLogical(masked, hull.builder.Constant(3u)))); + return RefusedBecause(hull.program, "not affine"); }); return passed ? 0 : 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index a861172480..db0c9b7d38 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -55,6 +55,16 @@ alignas(256) constexpr std::array<std::uint32_t, 49> HullCode{ 0xbf810000, }; +alignas(256) constexpr std::array<std::uint32_t, 51> HullMaskedControlPoint{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x362c02ff, 0x00001f00, 0x2c2c2c84, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, + 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, + 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, + 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, + 0xe0741000, 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array<std::uint32_t, 54> HullCompareBranch{ 0xd4040016, 0x00020080, 0xbeea0416, 0xbf870001, 0xbf800000, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, @@ -1129,6 +1139,7 @@ int main() { ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "outside its control points and patch constants", {{3u, 3u, 1u, 2u, 2u, 129u}, Indices}); RequirePatches(*device, target, HullCode, "hull program"); + RequirePatches(*device, target, HullMaskedControlPoint, "hull program masking its control point out of v1 for its off-chip record"); RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec"); RequirePatches(*device, target, HullNextInput, "hull program reading the next of 4 input control points of 24 bytes", NextInputTriangles, LocalCodeStride24); RequirePatches(*device, target, HullDynamicIndex, "hull program reading a control point at a computed index"); diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 7a6a4bec4f..4fcc7c94b6 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -440,7 +440,12 @@ private: const auto parts = split(value, divisor); if (!parts) return std::nullopt; if (parts->second.Scalar()) return Affine{0, 0, parts->second.constant & bits}; - return bits == divisor - 1 ? std::optional(parts->second) : std::nullopt; + if (bits == divisor - 1) return parts->second; + const auto lowest = 1ll << std::countr_zero(static_cast<std::uint64_t>(bits)); + if (bits != divisor - lowest) return std::nullopt; + const auto field = split(parts->second, lowest); + if (!field) return std::nullopt; + return field->first; } [[nodiscard]] std::optional<Affine> disjoint(const Affine& high, const Affine& low) const { From d4a25e115a66ef78e1b2a59705ef1903212dabb7 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Thu, 22 Oct 2026 14:51:34 +0400 Subject: [PATCH 487/702] feat(libc): export the ctype facet vtable as data Titles that import ctype<char>'s vtable as a 112-byte object need the fourteen-slot STT_OBJECT measured on PPSA16833 libc.prx. --- core/libs/prx/libc/src/LocaleSupport.cpp | 1 + core/libs/tests/GuestLocale.cpp | 9 +++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 11 insertions(+) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 3ed2a68736..90da00363c 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -591,6 +591,7 @@ int APS5_VABI tolower_nid_postfix(int c) { return ConvertCharacter(c, false); } std::uint64_t _ZNSt5ctypeIcE2idE_nid_postfix = 0; std::uint64_t _ZNSt5ctypeIwE2idE_nid_postfix = 0; +std::uintptr_t _ZTVSt5ctypeIcE_nid_postfix[14] {}; std::uint64_t _ZNSt7collateIwE2idE_nid_postfix = 0; std::uint64_t _ZNSt7collateIcE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7collateIcE_nid_postfix[9] { diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index f2ba945bb3..4f2da5e5e1 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -30,6 +30,7 @@ extern std::uintptr_t _ZTVSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsI extern std::uintptr_t _ZTVSt8time_getIwSt19istreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[13]; extern std::uintptr_t _ZTVSt7num_getIwSt19istreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[17]; extern std::uintptr_t _ZTVSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[14]; +extern std::uintptr_t _ZTVSt5ctypeIcE_nid_postfix[14]; } @@ -498,6 +499,13 @@ static void CheckFacetGetcat() { } Require(_ZNSt7codecvtIcc9_MbstatetE7_GetcatEPPKNSt6locale5facetEPKS2__nid_postfix(nullptr, &locale) == 2); Require(_ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&punct, &locale) == 4); +static void CheckCtypeVtableObject() { + for (std::uintptr_t& slot : _ZTVSt5ctypeIcE_nid_postfix) { + Require(slot == 0); + slot = 0x1111; + Require(slot == 0x1111); + slot = 0; + } } static void CheckNumPutVtableObject() { @@ -565,6 +573,7 @@ int main() { CheckTimeGetWideVtableObject(); CheckNumGetWideVtableObject(); CheckNumPutVtableObject(); + CheckCtypeVtableObject(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d272adb019..b58d6302dd 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -200,6 +200,7 @@ - [time_get<wchar_t> vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `sEca1nUOueA`) is thirteen zeroed `uintptr_t` slots matching `st_size` 104 on console `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and seven `do_*` virtuals). Facet calls through the object fault until those methods are wired. - [num_get<wchar_t> vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `Y9C9GeKyZ3A`) is seventeen zeroed `uintptr_t` slots matching `st_size` 136 on console `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and eleven `do_get` virtuals). Facet calls through the object fault until those methods are wired. - [num_put<char> vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `1kZFcktOm+s`) is fourteen zeroed `uintptr_t` slots matching `st_size` 112 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and eight `do_put` virtuals). Facet calls through the object fault until those methods are wired. +- [ctype<char> vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `GKWcAz6-G7k`) is fourteen zeroed `uintptr_t` slots matching `st_size` 112 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and eight `do_*` virtuals). Facet calls through the object fault until those methods are wired. - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. - [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), otherwise without console verification. `_Thrd_id` returns `scePthreadSelf()` unchanged, as measured on a PS5 (firmware 13.40) in two threads (#4164, #8711); `_Launch` omits `_Cnd_do_broadcast_at_thread_exit`; the two-argument form starts with default attributes and the three-argument form forwards the caller's attribute pointer, as PPSA02530's `libc.prx` does at `0x18090`. The 32-byte pad layout is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered - [siginterrupt](../../../core/libs/prx/libc/src/Process.cpp) (libc) runs FreeBSD 11.4's `siginterrupt.c` sequence through the `sigaction` and the `_sigintr` set that libkernel registers at load: it reads the signal's disposition, sets or clears the signal's bit and `SA_RESTART`, and installs the disposition again. The set is written outside libkernel's registration lock, as FreeBSD's libc writes it without one; a call before libkernel has registered throws. From a840d2f9e7711e6e3028d50dd15d37b45c6bbdc9 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Sun, 25 Oct 2026 23:40:51 +0400 Subject: [PATCH 488/702] fix(libc): restore separate GuestLocale vtable helpers --- core/libs/tests/GuestLocale.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 4f2da5e5e1..35c2a6ece2 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -499,6 +499,8 @@ static void CheckFacetGetcat() { } Require(_ZNSt7codecvtIcc9_MbstatetE7_GetcatEPPKNSt6locale5facetEPKS2__nid_postfix(nullptr, &locale) == 2); Require(_ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&punct, &locale) == 4); +} + static void CheckCtypeVtableObject() { for (std::uintptr_t& slot : _ZTVSt5ctypeIcE_nid_postfix) { Require(slot == 0); From 197919d28ca42478a3efc8363507462d0fc2776f Mon Sep 17 00:00:00 2001 From: Omar Elmahy <elma7e@ieee.org> Date: Tue, 27 Oct 2026 11:11:20 +0000 Subject: [PATCH 489/702] test(libSceGameUpdate): cover post-terminate request lifecycle and null entitlement validation --- core/libs/tests/GuestGameUpdate.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/libs/tests/GuestGameUpdate.cpp b/core/libs/tests/GuestGameUpdate.cpp index 34c5c5308b..eaeda50654 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -113,6 +113,7 @@ int main() { Require(sceGameUpdateGetAddcontLatestVersion(0, &label, &info) == notInitialized); Require(sceGameUpdateGetAddcontLatestVersion(0, &label, nullptr) == invalidArgument); Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); + Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, nullptr) == invalidArgument); Require(sceGameUpdateInitialize() == 0); Require(sceGameUpdateInitialize() == alreadyInitialized); Require(sceGameUpdateGetAddcontLatestVersion(0, &label, nullptr) == invalidArgument); @@ -133,4 +134,15 @@ int main() { Require(sceGameUpdateTerminate() == notInitialized); info.size = sizeof(info); Require(sceGameUpdateGetAddcontLatestVersion(0, &label, &info) == notInitialized); + Require(sceGameUpdateGetAddcontLatestVersion(0, &label, nullptr) == invalidArgument); + Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); + Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, nullptr) == invalidArgument); + Require(sceGameUpdateCreateRequest() == notInitialized); + GameUpdateCheckParam postParam{}; + postParam.size = sizeof(postParam); + GameUpdateCheckResult postResult{}; + postResult.size = sizeof(postResult); + Require(sceGameUpdateCheck(0x20000000, &postParam, &postResult) == notInitialized); + Require(sceGameUpdateAbortRequest(0x20000000) == notInitialized); + Require(sceGameUpdateDeleteRequest(0x20000000) == notInitialized); } From bece5871e268357d31ab8f17fd1293eec5ce3520 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 27 Oct 2026 07:01:48 -0400 Subject: [PATCH 490/702] fix(libc): keep NaN payloads in strtof and wcstof as titles' libc.prx does strtof and wcstof returned the default quiet NaN for any NAN(...) sequence. The libc.prx bundled with eleven titles reads the payload as its strtod does, but into one 32-bit word: a blank after the first group of digits ends it, the first 8 digits are kept and cut to 24 bits, the result goes under the quiet bit with the sign, and a zero payload becomes 1. HexNanPayload now takes the word count, two for double and one for float, and the NaN table test checks the measured float bits and end for all 64 inputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Strings.cpp | 23 ++-- core/libs/tests/GuestMath.cpp | 150 ++++++++++++++------------- core/libs/tests/GuestWideStrings.cpp | 2 +- docs/dev/debt/libc.md | 3 +- 4 files changed, 96 insertions(+), 82 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 128c85cd7e..bff8d1cf97 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -80,9 +80,10 @@ void ShiftNanWords(std::uint32_t* words, int from, int to, int digits) { } while (++index < to); } +template<int Words> std::uint64_t HexNanPayload(const char* open, const char*& end) { - std::uint32_t words[2] = {}; - constexpr int high = 1; + std::uint32_t words[Words] = {}; + constexpr int high = Words - 1; int top = high; int group = high; int digits = 0; @@ -143,10 +144,15 @@ std::uint64_t HexNanPayload(const char* open, const char*& end) { do words[to++] = 0; while (to <= high); } else { - words[high] &= 0xffffffffu >> 12; + words[high] &= 0xffffffffu >> (Words == 2 ? 12 : 8); + } + if constexpr (Words == 2) { + if (words[0] == 0 && words[1] == 0) words[0] = 1; + return (static_cast<std::uint64_t>(words[1]) << 32) | words[0]; + } else { + if (words[0] == 0) words[0] = 1; + return words[0]; } - if (words[0] == 0 && words[1] == 0) words[0] = 1; - return (static_cast<std::uint64_t>(words[1]) << 32) | words[0]; } const char* NanSequenceEnd(const char* tail) { @@ -160,11 +166,12 @@ template<typename TFloat> TFloat NanValue(const NanSubject& subject, const char*& end) { if constexpr (std::is_same_v<TFloat, double>) { end = subject.tail; - const std::uint64_t payload = *subject.tail == '(' ? HexNanPayload(subject.tail, end) : 0; + const std::uint64_t payload = *subject.tail == '(' ? HexNanPayload<2>(subject.tail, end) : 0; return std::bit_cast<double>((subject.negative ? 0x8000000000000000ull : 0) | 0x7ff8000000000000ull | payload); } else if constexpr (std::is_same_v<TFloat, float>) { - end = NanSequenceEnd(subject.tail); - return std::bit_cast<float>((subject.negative ? 0x80000000u : 0) | 0x7fc00000u); + end = subject.tail; + const auto payload = static_cast<std::uint32_t>(*subject.tail == '(' ? HexNanPayload<1>(subject.tail, end) : 0); + return std::bit_cast<float>((subject.negative ? 0x80000000u : 0) | 0x7fc00000u | payload); } else { end = NanSequenceEnd(subject.tail); TFloat value{}; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index f8e0f9da06..9ae64bf560 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -309,72 +309,74 @@ static void CheckNanConversions() { bool extendedMeasured; bool wideDoubleMeasured; bool wideOtherMeasured; + std::uint32_t singleBits; + std::size_t singleUsed; }; static constexpr MeasuredNan cases[] = { - {"nan", 0x7ff8000000000000ull, 3, 3, true, true, true}, - {"NAN", 0x7ff8000000000000ull, 3, 3, false, false, false}, - {"nan()", 0x7ff8000000000000ull, 5, 5, false, false, false}, - {"nan(1)", 0x7ff8000000000001ull, 6, 6, true, true, true}, - {"nan(0x10)", 0x7ff8000000000010ull, 9, 9, false, false, false}, - {"nan(abc)", 0x7ff8000000000abcull, 8, 8, false, false, false}, - {"nan(", 0x7ff8000000000000ull, 3, 3, true, true, true}, - {"-nan", 0xfff8000000000000ull, 4, 4, true, true, true}, - {"+nan(5)", 0x7ff8000000000005ull, 7, 7, false, false, false}, - {"nan(10)", 0x7ff8000000000010ull, 7, 7, false, false, false}, - {"nan(xyz)", 0x7ff8000000000000ull, 8, 8, false, false, false}, - {"nan(_)", 0x7ff8000000000000ull, 6, 6, false, false, false}, - {"nan(12", 0x7ff8000000000012ull, 3, 3, false, false, false}, - {"-nan(5)", 0xfff8000000000005ull, 7, 7, false, false, false}, - {"nan(0x8000000000000)", 0x7ff8000000000000ull, 20, 20, false, false, false}, - {"nan(0x10000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, - {"nan(0x400000)", 0x7ff8000000400000ull, 13, 13, false, false, false}, - {"NaN(0X1F)", 0x7ff800000000001full, 9, 9, false, false, false}, - {"nan(0x)", 0x7ff8000000000000ull, 7, 7, false, false, false}, - {"nan(123abc)", 0x7ff8000000123abcull, 11, 11, true, true, false}, - {"nan(1x2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, - {"nan(0x1g)", 0x7ff8000000000000ull, 9, 9, true, true, false}, - {"nan(g1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, - {"nan(0x0x1)", 0x7ff8000000000000ull, 10, 10, true, true, false}, - {"nan(fffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, - {"nan(1fffffffffffff)", 0x7fffffffffffffffull, 19, 19, true, true, false}, - {"nan(10000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, - {"nan(20000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, - {"nan(0x20000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, - {"nan(ffffffffffffffff)", 0x7fffffffffffffffull, 21, 21, true, true, false}, - {"nan(10000000000000000)", 0x7ff8000000000001ull, 22, 22, true, true, false}, - {"nan(0X)", 0x7ff8000000000000ull, 7, 7, true, true, false}, - {"nan(-1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, - {"nan(+1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, - {"nan( 1)", 0x7ff8000000000001ull, 7, 3, true, true, false}, - {"nan(1 )", 0x7ff8000100000000ull, 7, 3, true, true, false}, - {"nan(0)", 0x7ff8000000000001ull, 6, 6, true, true, false}, - {"nan(00001)", 0x7ff8000000000001ull, 10, 10, true, true, false}, - {"nan(0x0001)", 0x7ff8000000000001ull, 11, 11, true, true, false}, - {"nan(8000000000000)", 0x7ff8000000000000ull, 18, 18, true, true, false}, - {"nan(7ffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, - {"nan(123", 0x7ff8000000000123ull, 3, 3, true, true, false}, - {"nan(1)x", 0x7ff8000000000001ull, 6, 6, true, true, false}, - {"-nan(1)", 0xfff8000000000001ull, 7, 7, true, true, false}, - {"nan(abcdefABCDEF)", 0x7ff8abcdefabcdefull, 17, 17, true, true, false}, - {"nan(0xABCDEF)", 0x7ff8000000abcdefull, 13, 13, true, true, false}, - {"nan(9)", 0x7ff8000000000009ull, 6, 6, true, true, false}, - {"nan(1_2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, - {"nan(_1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, - {"nan(1)(2)", 0x7ff8000000000001ull, 6, 6, true, true, false}, - {"+nan", 0x7ff8000000000000ull, 4, 4, true, true, true}, - {"NaN", 0x7ff8000000000000ull, 3, 3, true, true, true}, - {"-NAN(5)", 0xfff8000000000005ull, 7, 7, true, true, true}, - {"nan(5)", 0x7ff8000000000005ull, 6, 6, true, true, true}, - {"-nan(abc)", 0xfff8000000000abcull, 9, 9, true, true, true}, - {" -nan", 0xfff8000000000000ull, 6, 6, true, true, true}, - {"nan(12 x", 0x7ff8000000000000ull, 3, 3, true, true, true}, - {"nan(12x", 0x7ff8000000000000ull, 3, 3, true, true, true}, - {"nan(1", 0x7ff8000000000001ull, 3, 3, true, true, true}, - {"nan(a b)", 0x7ff8000a0000000bull, 8, 3, true, true, true}, - {"nan(ab)", 0x7ff80000000000abull, 7, 7, true, true, true}, - {"nanx", 0x7ff8000000000000ull, 3, 3, true, true, true}, - {"-nan()", 0xfff8000000000000ull, 6, 6, true, true, true}, - {"nan(0x8000000000001)", 0x7ff8000000000001ull, 20, 20, true, true, true}, + {"nan", 0x7ff8000000000000ull, 3, 3, true, true, true, 0x7fc00000u, 3}, + {"NAN", 0x7ff8000000000000ull, 3, 3, false, false, false, 0x7fc00000u, 3}, + {"nan()", 0x7ff8000000000000ull, 5, 5, false, false, false, 0x7fc00000u, 5}, + {"nan(1)", 0x7ff8000000000001ull, 6, 6, true, true, true, 0x7fc00001u, 6}, + {"nan(0x10)", 0x7ff8000000000010ull, 9, 9, false, false, false, 0x7fc00010u, 9}, + {"nan(abc)", 0x7ff8000000000abcull, 8, 8, false, false, false, 0x7fc00abcu, 8}, + {"nan(", 0x7ff8000000000000ull, 3, 3, true, true, true, 0x7fc00000u, 3}, + {"-nan", 0xfff8000000000000ull, 4, 4, true, true, true, 0xffc00000u, 4}, + {"+nan(5)", 0x7ff8000000000005ull, 7, 7, false, false, false, 0x7fc00005u, 7}, + {"nan(10)", 0x7ff8000000000010ull, 7, 7, false, false, false, 0x7fc00010u, 7}, + {"nan(xyz)", 0x7ff8000000000000ull, 8, 8, false, false, false, 0x7fc00000u, 8}, + {"nan(_)", 0x7ff8000000000000ull, 6, 6, false, false, false, 0x7fc00000u, 6}, + {"nan(12", 0x7ff8000000000012ull, 3, 3, false, false, false, 0x7fc00012u, 3}, + {"-nan(5)", 0xfff8000000000005ull, 7, 7, false, false, false, 0xffc00005u, 7}, + {"nan(0x8000000000000)", 0x7ff8000000000000ull, 20, 20, false, false, false, 0x7fc00001u, 20}, + {"nan(0x10000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false, 0x7fc00001u, 21}, + {"nan(0x400000)", 0x7ff8000000400000ull, 13, 13, false, false, false, 0x7fc00000u, 13}, + {"NaN(0X1F)", 0x7ff800000000001full, 9, 9, false, false, false, 0x7fc0001fu, 9}, + {"nan(0x)", 0x7ff8000000000000ull, 7, 7, false, false, false, 0x7fc00000u, 7}, + {"nan(123abc)", 0x7ff8000000123abcull, 11, 11, true, true, false, 0x7fd23abcu, 11}, + {"nan(1x2)", 0x7ff8000000000000ull, 8, 8, true, true, false, 0x7fc00000u, 8}, + {"nan(0x1g)", 0x7ff8000000000000ull, 9, 9, true, true, false, 0x7fc00000u, 9}, + {"nan(g1)", 0x7ff8000000000000ull, 7, 7, true, true, false, 0x7fc00000u, 7}, + {"nan(0x0x1)", 0x7ff8000000000000ull, 10, 10, true, true, false, 0x7fc00000u, 10}, + {"nan(fffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false, 0x7fffffffu, 18}, + {"nan(1fffffffffffff)", 0x7fffffffffffffffull, 19, 19, true, true, false, 0x7fffffffu, 19}, + {"nan(10000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false, 0x7fc00001u, 19}, + {"nan(20000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false, 0x7fc00001u, 19}, + {"nan(0x20000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false, 0x7fc00001u, 21}, + {"nan(ffffffffffffffff)", 0x7fffffffffffffffull, 21, 21, true, true, false, 0x7fffffffu, 21}, + {"nan(10000000000000000)", 0x7ff8000000000001ull, 22, 22, true, true, false, 0x7fc00001u, 22}, + {"nan(0X)", 0x7ff8000000000000ull, 7, 7, true, true, false, 0x7fc00000u, 7}, + {"nan(-1)", 0x7ff8000000000000ull, 7, 3, true, true, false, 0x7fc00000u, 7}, + {"nan(+1)", 0x7ff8000000000000ull, 7, 3, true, true, false, 0x7fc00000u, 7}, + {"nan( 1)", 0x7ff8000000000001ull, 7, 3, true, true, false, 0x7fc00001u, 7}, + {"nan(1 )", 0x7ff8000100000000ull, 7, 3, true, true, false, 0x7fc00001u, 7}, + {"nan(0)", 0x7ff8000000000001ull, 6, 6, true, true, false, 0x7fc00001u, 6}, + {"nan(00001)", 0x7ff8000000000001ull, 10, 10, true, true, false, 0x7fc00001u, 10}, + {"nan(0x0001)", 0x7ff8000000000001ull, 11, 11, true, true, false, 0x7fc00001u, 11}, + {"nan(8000000000000)", 0x7ff8000000000000ull, 18, 18, true, true, false, 0x7fc00001u, 18}, + {"nan(7ffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false, 0x7fffffffu, 18}, + {"nan(123", 0x7ff8000000000123ull, 3, 3, true, true, false, 0x7fc00123u, 3}, + {"nan(1)x", 0x7ff8000000000001ull, 6, 6, true, true, false, 0x7fc00001u, 6}, + {"-nan(1)", 0xfff8000000000001ull, 7, 7, true, true, false, 0xffc00001u, 7}, + {"nan(abcdefABCDEF)", 0x7ff8abcdefabcdefull, 17, 17, true, true, false, 0x7fcdefabu, 17}, + {"nan(0xABCDEF)", 0x7ff8000000abcdefull, 13, 13, true, true, false, 0x7febcdefu, 13}, + {"nan(9)", 0x7ff8000000000009ull, 6, 6, true, true, false, 0x7fc00009u, 6}, + {"nan(1_2)", 0x7ff8000000000000ull, 8, 8, true, true, false, 0x7fc00000u, 8}, + {"nan(_1)", 0x7ff8000000000000ull, 7, 7, true, true, false, 0x7fc00000u, 7}, + {"nan(1)(2)", 0x7ff8000000000001ull, 6, 6, true, true, false, 0x7fc00001u, 6}, + {"+nan", 0x7ff8000000000000ull, 4, 4, true, true, true, 0x7fc00000u, 4}, + {"NaN", 0x7ff8000000000000ull, 3, 3, true, true, true, 0x7fc00000u, 3}, + {"-NAN(5)", 0xfff8000000000005ull, 7, 7, true, true, true, 0xffc00005u, 7}, + {"nan(5)", 0x7ff8000000000005ull, 6, 6, true, true, true, 0x7fc00005u, 6}, + {"-nan(abc)", 0xfff8000000000abcull, 9, 9, true, true, true, 0xffc00abcu, 9}, + {" -nan", 0xfff8000000000000ull, 6, 6, true, true, true, 0xffc00000u, 6}, + {"nan(12 x", 0x7ff8000000000000ull, 3, 3, true, true, true, 0x7fc00000u, 3}, + {"nan(12x", 0x7ff8000000000000ull, 3, 3, true, true, true, 0x7fc00000u, 3}, + {"nan(1", 0x7ff8000000000001ull, 3, 3, true, true, true, 0x7fc00001u, 3}, + {"nan(a b)", 0x7ff8000a0000000bull, 8, 3, true, true, true, 0x7fc0000au, 8}, + {"nan(ab)", 0x7ff80000000000abull, 7, 7, true, true, true, 0x7fc000abu, 7}, + {"nanx", 0x7ff8000000000000ull, 3, 3, true, true, true, 0x7fc00000u, 3}, + {"-nan()", 0xfff8000000000000ull, 6, 6, true, true, true, 0xffc00000u, 6}, + {"nan(0x8000000000001)", 0x7ff8000000000001ull, 20, 20, true, true, true, 0x7fc00001u, 20}, }; bool correct = true; for (const auto& test : cases) { @@ -388,9 +390,8 @@ static void CheckNanConversions() { std::fprintf(stderr, "atof(\"%s\")\n", test.text); correct = false; } - const std::uint32_t expectedSingle = (test.bits >> 63 != 0 ? 0x80000000u : 0) | 0x7fc00000u; const auto single = std::bit_cast<std::uint32_t>(strtof_nid_postfix(test.text, &end)); - if (single != expectedSingle || end != test.text + test.sequenceUsed) { + if (single != test.singleBits || end != test.text + test.singleUsed) { std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast<unsigned>(single)); correct = false; } @@ -404,6 +405,11 @@ static void CheckNanConversions() { char16_t wide[64] = {}; for (std::size_t i = 0; test.text[i] != 0; ++i) wide[i] = static_cast<unsigned char>(test.text[i]); char16_t* wideEnd = nullptr; + const auto wideSingle = std::bit_cast<std::uint32_t>(wcstof_nid_postfix(wide, &wideEnd)); + if (wideSingle != test.singleBits || wideEnd != wide + test.singleUsed) { + std::fprintf(stderr, "wcstof(\"%s\") = 0x%08x\n", test.text, static_cast<unsigned>(wideSingle)); + correct = false; + } if (test.wideDoubleMeasured) { const auto wideValue = std::bit_cast<std::uint64_t>(wcstod_nid_postfix(wide, &wideEnd)); if (wideValue != test.bits || wideEnd != wide + test.used) { @@ -412,11 +418,9 @@ static void CheckNanConversions() { } } if (test.wideOtherMeasured) { - const auto wideSingle = std::bit_cast<std::uint32_t>(wcstof_nid_postfix(wide, &wideEnd)); - const bool singleOk = wideSingle == expectedSingle && wideEnd == wide + test.sequenceUsed; const long double wideExtended = wcstold_nid_postfix(wide, &wideEnd); - if (!singleOk || !ExtendedBits(wideExtended, 0xc000000000000000ull, 0x7fff) || wideEnd != wide + test.sequenceUsed) { - std::fprintf(stderr, "wcstof/wcstold(\"%s\")\n", test.text); + if (!ExtendedBits(wideExtended, 0xc000000000000000ull, 0x7fff) || wideEnd != wide + test.sequenceUsed) { + std::fprintf(stderr, "wcstold(\"%s\")\n", test.text); correct = false; } } @@ -1923,10 +1927,12 @@ static void CheckNoConversionZero() { Require(parsedFloat == 0.f && std::signbit(parsedFloat) && end == text); Require(!std::signbit(strtold_nid_postfix(text, nullptr))); } - for (const char* text : {"-nan", "-NAN", " -nan(5)"}) { + struct SignedNan { const char* text; std::uint32_t bits; }; + for (const SignedNan& signedNan : {SignedNan{"-nan", 0xffc00000u}, SignedNan{"-NAN", 0xffc00000u}, SignedNan{" -nan(5)", 0xffc00005u}}) { + const char* const text = signedNan.text; char* end = nullptr; const float parsedFloat = strtof_nid_postfix(text, &end); - Require(std::bit_cast<std::uint32_t>(parsedFloat) == 0xffc00000u && end == text + std::strlen(text)); + Require(std::bit_cast<std::uint32_t>(parsedFloat) == signedNan.bits && end == text + std::strlen(text)); } Require(std::bit_cast<std::uint32_t>(strtof_nid_postfix("nan", nullptr)) == 0x7fc00000u); char* end = nullptr; diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index e31bb8ed9b..f8cf9ce972 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -343,7 +343,7 @@ int main() { require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; require(std::bit_cast<std::uint64_t>(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000123u && end == wideNan + 12); - require(std::bit_cast<std::uint32_t>(wcstof_nid_postfix(wideNan, &end)) == 0xffc00000u && end == wideNan + 12); + require(std::bit_cast<std::uint32_t>(wcstof_nid_postfix(wideNan, &end)) == 0xffc00123u && end == wideNan + 12); const long double wideExtended = wcstold_nid_postfix(wideNan, &end); std::uint64_t significand; std::memcpy(&significand, &wideExtended, sizeof(significand)); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c3a5564270..87d9d400f5 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -108,7 +108,8 @@ - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [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. The multibyte conversions follow the C locale of the console's `libSceLibcInternal.sprx`: UTF-8 of at most three bytes, so `_Getmbcurmax` returns 3, overlong forms and encoded surrogates decode, and four-byte forms are invalid. `__mb_cur_max` stays 1 like the console's variable; `___mb_cur_max` is not in that module and also stays 1, unverified. Other locales are not supported. With a null destination, `wcstombs` returns the byte count; the console's count is wrong when the capacity argument equals the running count, because it reads past the terminator - [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) with two gaps. A host symbolic link with a relative target under the guest root is expanded as `realpath1` does (a dangling link fails with `ENOENT`, more than 32 links with `ELOOP`), but a link with an absolute target is not expanded, nor is anything below it, because the target is a host path with no guest path to map to. Only Windows symbolic links are expanded there, not junctions. Expansion works on the guest path, so a host link whose text climbs above a path alias root is resolved against the guest tree, not the host one. Otherwise `..` drops the previous component lexically, as `Resolve` does for every guest path. -- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there +- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtold` and `wcstold` return the default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) whatever the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; titles' own `libc.prx` copies give the same `strtod` results. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there +- [strtof and wcstof NaN payloads](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - the console's `libSceLibcInternal.sprx` (firmware 12.70) returns the default NaN for every `NAN(...)` sequence, and modules that import it reach this function through the `libc.prx` fallback, so they get the payload the titles' copy keeps. That copy, in eleven titles (64 inputs), reads the sequence as `strtod` does but into one 32-bit word: a blank after the first group of digits ends the payload, the first 8 digits are kept and cut to 24 bits under the quiet bit, and a zero payload becomes 1. - [strtod](../../../core/libs/prx/libc/src/Strings.cpp) (libc) sets `ERANGE` for tested decimal inputs with a nonzero subnormal result, including the exact decimal expansion of 2^-1074, matching nine distinct title-bundled `libc.prx` builds measured on Windows. Two other `libc.prx` builds and the console's `libSceLibcInternal.sprx` leave `errno` alone for decimal subnormals. `wcstod` still parses through the host `std::strtod` path without this explicit rule; title and console `wcstod` behavior for decimal subnormals is unverified. The title-bundled hexadecimal subnormal rule remains unverified. - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [_Stod, _Stodx, _Stofx, _Stoldx, _Stof and _Stold](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throw for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999. A nonzero subnormal result sets neither `errno` nor the error argument, as measured in firmware 13.60's `libSceLibcInternal.sprx` run in a Linux host process, not on a console; there about a fifth of decimal inputs with a subnormal result come out one unit apart in the last place (`_Stodx("2.2250738585072014e-308", 0)` is the largest subnormal), which is not reproduced From 58f12f318caec3a6178bdc5d10d9dcc18416eb25 Mon Sep 17 00:00:00 2001 From: Alvaro <alvaropobladoresteban9@gmail.com> Date: Tue, 27 Oct 2026 12:24:00 +0100 Subject: [PATCH 491/702] test(libkernel_sys): drop the Windows linkat symbolic link throw assertion The Windows branch only pinned that linkat throws for a symbolic link without AT_SYMLINK_FOLLOW, an unsupported path. The POSIX checks of the real behaviour and the argument and error checks stay. --- core/libs/tests/GuestKernelSys.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 1979fe5248..cb97824942 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -146,9 +146,7 @@ void OwnershipAndLinkTests(const std::filesystem::path& root) { Require(lchown_nid_postfix(dangling.c_str(), 0, 0) == 0); Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0) == -1 && Error() == 2); Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0x200) == 0); -#ifdef _WIN32 - Require(ThrowsNaming([&] { linkat_nid_postfix(-100, symbolic.c_str(), -100, other.c_str(), 0); }, "symbolic link itself")); -#else +#ifndef _WIN32 const auto symbolicCopy = (root / "copy.link").string(); const auto danglingCopy = (root / "dangling.copy").string(); Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, symbolicCopy.c_str(), 0) == 0); From c2a6577ad9ea29057573afdff6e1326f931f7a41 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 08:21:56 +0000 Subject: [PATCH 492/702] docs(libSceAgcDriver): list the registration init and memory query as silent stubs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c8424c9a73..28afd52220 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -2,6 +2,7 @@ ### Silent stubs +- [sceAgcDriverInitResourceRegistration, sceAgcDriverQueryResourceRegistrationUserMemoryRequirements](../../../core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp) (libSceAgcDriver) - the init call returns 0 without setting anything up. The query writes 16 KiB (0x4000) as the user memory registration needs and returns 0; I found no source for that size. Registration stays unavailable (see Resource registration), so the size is never used. Crimson Desert (PPSA25286), Call of Duty: Black Ops II (PPSA34502), Steins;Gate Re:Boot (PPSA36591) and Metal Gear Solid 4 (PPSA18194) reach both through one-instruction wrappers. - [sceAgcDriverRegisterDefaultOwner](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - takes one uint32 owner by value (OpenAGC `include/agcdriver.h`, SharpEmu `AgcExports.DriverResources.cs`) and returns 0 without recording it. PPSA21480 treats the code 0x8A6C9018 it returned before as fatal. Console behaviour is unverified: OpenAGC describes the function as a firmware 5.50 stub that returns 0x8A6C9018, SharpEmu returns 0. The one call in PPSA21480 (0x7f41f) is followed by a test of the sign of the result only; a negative result ends in a coredump user string write and `abort()` (0x131d0-0x13214). - [sceAgcDriverSetResourceRegistrationInfo](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - returns 0 without storing the info, as the retail `libSceAgcDriver.sprx` body does; registration is unavailable, so nothing reads it back. From c60f0fc43c27790e7a48fa54d1c8526073269d40 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 08:48:04 +0000 Subject: [PATCH 493/702] docs(libSceAgcDriver): note the registration memory query's null-output refusal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 28afd52220..63673e7557 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sceAgcDriverInitResourceRegistration, sceAgcDriverQueryResourceRegistrationUserMemoryRequirements](../../../core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp) (libSceAgcDriver) - the init call returns 0 without setting anything up. The query writes 16 KiB (0x4000) as the user memory registration needs and returns 0; I found no source for that size. Registration stays unavailable (see Resource registration), so the size is never used. Crimson Desert (PPSA25286), Call of Duty: Black Ops II (PPSA34502), Steins;Gate Re:Boot (PPSA36591) and Metal Gear Solid 4 (PPSA18194) reach both through one-instruction wrappers. +- [sceAgcDriverInitResourceRegistration, sceAgcDriverQueryResourceRegistrationUserMemoryRequirements](../../../core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp) (libSceAgcDriver) - the init call returns 0 without setting anything up. The query writes 16 KiB (0x4000) as the user memory registration needs and returns 0, and a null output returns `AGC_EINVAL` (0x80020065) without writing. I found no source for that size. Registration stays unavailable (see Resource registration), so the size is never used. Crimson Desert (PPSA25286), Call of Duty: Black Ops II (PPSA34502), Steins;Gate Re:Boot (PPSA36591) and Metal Gear Solid 4 (PPSA18194) reach both through one-instruction wrappers. - [sceAgcDriverRegisterDefaultOwner](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - takes one uint32 owner by value (OpenAGC `include/agcdriver.h`, SharpEmu `AgcExports.DriverResources.cs`) and returns 0 without recording it. PPSA21480 treats the code 0x8A6C9018 it returned before as fatal. Console behaviour is unverified: OpenAGC describes the function as a firmware 5.50 stub that returns 0x8A6C9018, SharpEmu returns 0. The one call in PPSA21480 (0x7f41f) is followed by a test of the sign of the result only; a negative result ends in a coredump user string write and `abort()` (0x131d0-0x13214). - [sceAgcDriverSetResourceRegistrationInfo](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - returns 0 without storing the info, as the retail `libSceAgcDriver.sprx` body does; registration is unavailable, so nothing reads it back. From 737d4dea35df46d377884656574f955a46ec3f6b Mon Sep 17 00:00:00 2001 From: Brainiac91 <alin.negut@gmail.com> Date: Mon, 26 Oct 2026 10:06:02 +0200 Subject: [PATCH 494/702] fix(allocator): reject malformed import metadata in PeImportFirst Read the Import Directory entry only when the optional header holds it and NumberOfRvaAndSizes declares it. Require every import name to end with a NUL inside the file and resolve the names after the descriptor table is bounds-checked. --- core/libs/allocator/PeImportFirst.cpp | 20 +++++++++++++------ .../allocator/tests/PeImportFirstTests.cpp | 16 +++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/core/libs/allocator/PeImportFirst.cpp b/core/libs/allocator/PeImportFirst.cpp index aa4b040ba2..dc4115b8d6 100644 --- a/core/libs/allocator/PeImportFirst.cpp +++ b/core/libs/allocator/PeImportFirst.cpp @@ -42,7 +42,9 @@ void MoveImportFirst(std::vector<unsigned char>& bytes, const std::string& dll) const auto optionalSize = read16(bytes, pe + 20); const auto optional = pe + 24; if (read16(bytes, optional) != 0x20bu) throw std::runtime_error("not a PE32+ image"); - const auto importRva = read32(bytes, optional + 112 + 8); + constexpr std::size_t ImportDirectory = 112 + 8; + if (optionalSize < ImportDirectory + 16 || read32(bytes, optional + 108) < 2) throw std::runtime_error("the image has no imports"); + const auto importRva = read32(bytes, optional + ImportDirectory); if (importRva == 0) throw std::runtime_error("the image has no imports"); const auto sectionTable = optional + optionalSize; const auto fileOffset = [&](std::uint32_t rva) -> std::size_t { @@ -62,18 +64,24 @@ void MoveImportFirst(std::vector<unsigned char>& bytes, const std::string& dll) const auto directory = fileOffset(importRva); constexpr std::size_t DescriptorBytes = 20; std::vector<std::array<unsigned char, DescriptorBytes>> descriptors; - std::size_t found = SIZE_MAX; for (std::size_t offset = directory;; offset += DescriptorBytes) { if (offset + DescriptorBytes > bytes.size()) throw std::runtime_error("truncated import directory"); std::array<unsigned char, DescriptorBytes> descriptor{}; std::memcpy(descriptor.data(), bytes.data() + offset, DescriptorBytes); if (std::all_of(descriptor.begin(), descriptor.end(), [](unsigned char value) { return value == 0; })) break; - const auto nameOffset = fileOffset(read32(bytes, offset + 12)); - std::string name; - for (auto at = nameOffset; at < bytes.size() && bytes[at] != 0; ++at) name.push_back(static_cast<char>(bytes[at])); - if (lower(name) == lower(dll)) found = descriptors.size(); descriptors.push_back(descriptor); } + std::size_t found = SIZE_MAX; + for (std::size_t index = 0; index < descriptors.size(); ++index) { + std::uint32_t nameRva = 0; + std::memcpy(&nameRva, descriptors[index].data() + 12, sizeof(nameRva)); + const auto nameOffset = fileOffset(nameRva); + if (nameOffset >= bytes.size()) throw std::runtime_error("RVA outside the file data"); + const auto terminator = std::find(bytes.begin() + static_cast<std::ptrdiff_t>(nameOffset), bytes.end(), 0); + if (terminator == bytes.end()) throw std::runtime_error("unterminated import name"); + const std::string name(bytes.begin() + static_cast<std::ptrdiff_t>(nameOffset), terminator); + if (lower(name) == lower(dll)) found = index; + } if (found == SIZE_MAX) throw std::runtime_error("the image does not import " + dll); std::rotate(descriptors.begin(), descriptors.begin() + static_cast<std::ptrdiff_t>(found), descriptors.begin() + static_cast<std::ptrdiff_t>(found) + 1); for (std::size_t index = 0; index < descriptors.size(); ++index) std::memcpy(bytes.data() + directory + index * DescriptorBytes, descriptors[index].data(), DescriptorBytes); diff --git a/core/libs/allocator/tests/PeImportFirstTests.cpp b/core/libs/allocator/tests/PeImportFirstTests.cpp index 5d40857cdc..df5dd83483 100644 --- a/core/libs/allocator/tests/PeImportFirstTests.cpp +++ b/core/libs/allocator/tests/PeImportFirstTests.cpp @@ -145,6 +145,22 @@ int main() { truncated.resize(SectionOffset + DescriptorBytes + DescriptorBytes / 2); failsWithoutTouchingTheFile(directory, "truncated-directory", truncated, "truncated import directory"); failsWithoutTouchingTheFile(directory, "pe32", image({"kernel32.dll", "mimalloc-redirect.dll"}, 0x10b), "not a PE32+ image"); + auto undeclared = image({"kernel32.dll", "mimalloc-redirect.dll"}); + put32(undeclared, OptionalOffset + 108, 0); + failsWithoutTouchingTheFile(directory, "directory-count-zero", undeclared, "the image has no imports"); + auto oneDirectory = image({"kernel32.dll", "mimalloc-redirect.dll"}); + put32(oneDirectory, OptionalOffset + 108, 1); + failsWithoutTouchingTheFile(directory, "directory-count-one", oneDirectory, "the image has no imports"); + auto shortHeader = image({"kernel32.dll", "mimalloc-redirect.dll"}); + put16(shortHeader, PeOffset + 20, 112); + failsWithoutTouchingTheFile(directory, "optional-header-without-imports", shortHeader, "the image has no imports"); + auto unterminated = image({"kernel32.dll", "mimalloc-redirect.dll"}); + unterminated.resize(SectionOffset + 3 * DescriptorBytes + 13 + 21); + failsWithoutTouchingTheFile(directory, "unterminated-name", unterminated, "unterminated import name"); + auto outsideFile = image({"kernel32.dll", "mimalloc-redirect.dll", "ucrtbase.dll"}); + put32(outsideFile, SectionOffset + 2 * DescriptorBytes + 12, static_cast<std::uint32_t>(SectionRva + 0x1ff)); + outsideFile.resize(0x300); + failsWithoutTouchingTheFile(directory, "name-past-the-file", outsideFile, "RVA outside the file data"); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; std::error_code ignored; From b729a37dd022522269058b7e394ecda2a248eba3 Mon Sep 17 00:00:00 2001 From: Brainiac91 <alin.negut@gmail.com> Date: Mon, 26 Oct 2026 16:07:46 +0200 Subject: [PATCH 495/702] fix(allocator): accept minimal PE32+ import headers --- core/libs/allocator/PeImportFirst.cpp | 2 +- .../allocator/tests/PeImportFirstTests.cpp | 44 +++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/core/libs/allocator/PeImportFirst.cpp b/core/libs/allocator/PeImportFirst.cpp index dc4115b8d6..cb4f561d11 100644 --- a/core/libs/allocator/PeImportFirst.cpp +++ b/core/libs/allocator/PeImportFirst.cpp @@ -43,7 +43,7 @@ void MoveImportFirst(std::vector<unsigned char>& bytes, const std::string& dll) const auto optional = pe + 24; if (read16(bytes, optional) != 0x20bu) throw std::runtime_error("not a PE32+ image"); constexpr std::size_t ImportDirectory = 112 + 8; - if (optionalSize < ImportDirectory + 16 || read32(bytes, optional + 108) < 2) throw std::runtime_error("the image has no imports"); + if (optionalSize < ImportDirectory + 8 || read32(bytes, optional + 108) < 2) throw std::runtime_error("the image has no imports"); const auto importRva = read32(bytes, optional + ImportDirectory); if (importRva == 0) throw std::runtime_error("the image has no imports"); const auto sectionTable = optional + optionalSize; diff --git a/core/libs/allocator/tests/PeImportFirstTests.cpp b/core/libs/allocator/tests/PeImportFirstTests.cpp index df5dd83483..62ff951c4d 100644 --- a/core/libs/allocator/tests/PeImportFirstTests.cpp +++ b/core/libs/allocator/tests/PeImportFirstTests.cpp @@ -17,7 +17,6 @@ namespace { constexpr std::size_t PeOffset = 0x40; constexpr std::size_t OptionalOffset = PeOffset + 24; constexpr std::size_t OptionalBytes = 240; -constexpr std::size_t SectionTable = OptionalOffset + OptionalBytes; constexpr std::uint32_t SectionRva = 0x1000; constexpr std::size_t SectionOffset = 0x200; constexpr std::size_t SectionBytes = 0x200; @@ -41,7 +40,7 @@ std::uint32_t get32(const std::vector<unsigned char>& bytes, std::size_t offset) return value; } -std::vector<unsigned char> image(const std::vector<std::string>& dlls, std::uint16_t magic = 0x20b) { +std::vector<unsigned char> image(const std::vector<std::string>& dlls, std::uint16_t magic = 0x20b, std::uint32_t directories = 16, std::uint16_t optionalBytes = OptionalBytes) { std::vector<unsigned char> bytes(SectionOffset + SectionBytes, 0); bytes[0] = 'M'; bytes[1] = 'Z'; @@ -49,16 +48,17 @@ std::vector<unsigned char> image(const std::vector<std::string>& dlls, std::uint put32(bytes, PeOffset, 0x00004550u); put16(bytes, PeOffset + 4, 0x8664); put16(bytes, PeOffset + 6, 1); - put16(bytes, PeOffset + 20, OptionalBytes); + put16(bytes, PeOffset + 20, optionalBytes); put16(bytes, OptionalOffset, magic); - put32(bytes, OptionalOffset + 108, 16); + put32(bytes, OptionalOffset + 108, directories); put32(bytes, OptionalOffset + 112 + 8, SectionRva); put32(bytes, OptionalOffset + 112 + 12, static_cast<std::uint32_t>((dlls.size() + 1) * DescriptorBytes)); - std::memcpy(bytes.data() + SectionTable, ".idata", 6); - put32(bytes, SectionTable + 8, SectionBytes); - put32(bytes, SectionTable + 12, SectionRva); - put32(bytes, SectionTable + 16, SectionBytes); - put32(bytes, SectionTable + 20, SectionOffset); + const auto sectionTable = OptionalOffset + optionalBytes; + std::memcpy(bytes.data() + sectionTable, ".idata", 6); + put32(bytes, sectionTable + 8, SectionBytes); + put32(bytes, sectionTable + 12, SectionRva); + put32(bytes, sectionTable + 16, SectionBytes); + put32(bytes, sectionTable + 20, SectionOffset); auto name = (dlls.size() + 1) * DescriptorBytes; for (std::size_t index = 0; index < dlls.size(); ++index) { const auto descriptor = SectionOffset + index * DescriptorBytes; @@ -110,6 +110,28 @@ void movesTheNamedImportFirst() { require(unchanged == original, "moving the first import changed the image"); } +void movesTheImportFirstWithATwoDirectoryOptionalHeader(const std::filesystem::path& directory) { + const auto original = image({"kernel32.dll", "ucrtbase.dll", "mimalloc-redirect.dll", "advapi32.dll"}, 0x20b, 2, 128); + auto moved = original; + Allocator::MoveImportFirst(moved, "mimalloc-redirect.dll"); + require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll" && importName(moved, 2) == "ucrtbase.dll" && importName(moved, 3) == "advapi32.dll", + "the import did not move first with a 128-byte optional header holding two directories"); + require(descriptor(moved, 0) == descriptor(original, 2) && descriptor(moved, 1) == descriptor(original, 0) && descriptor(moved, 2) == descriptor(original, 1) && descriptor(moved, 3) == descriptor(original, 3), + "a descriptor did not move whole with a 128-byte optional header"); + const auto directoryEnd = SectionOffset + 4 * DescriptorBytes; + require(std::equal(moved.begin(), moved.begin() + SectionOffset, original.begin()) && std::equal(moved.begin() + directoryEnd, moved.end(), original.begin() + directoryEnd), + "bytes outside the import descriptors changed with a 128-byte optional header"); + auto repeated = moved; + Allocator::MoveImportFirst(repeated, "mimalloc-redirect.dll"); + require(repeated == moved, "repeating the move changed the image with a 128-byte optional header"); + const auto path = directory / "two-directory.dll"; + writeFile(path, original); + Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll"); + require(readFile(path) == moved, "the file move differs from the in-memory move with a 128-byte optional header"); + Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll"); + require(readFile(path) == moved, "repeating the move changed the file with a 128-byte optional header"); +} + void movesTheImportInAFile(const std::filesystem::path& directory) { const auto path = directory / "moved.dll"; writeFile(path, image({"kernel32.dll", "mimalloc-redirect.dll"})); @@ -139,6 +161,7 @@ int main() { try { std::filesystem::create_directories(directory); movesTheNamedImportFirst(); + movesTheImportFirstWithATwoDirectoryOptionalHeader(directory); movesTheImportInAFile(directory); failsWithoutTouchingTheFile(directory, "missing-import", image({"kernel32.dll", "ucrtbase.dll"}), "does not import mimalloc-redirect.dll"); auto truncated = image({"kernel32.dll", "mimalloc-redirect.dll", "ucrtbase.dll"}); @@ -154,6 +177,9 @@ int main() { auto shortHeader = image({"kernel32.dll", "mimalloc-redirect.dll"}); put16(shortHeader, PeOffset + 20, 112); failsWithoutTouchingTheFile(directory, "optional-header-without-imports", shortHeader, "the image has no imports"); + auto incompleteEntry = image({"kernel32.dll", "mimalloc-redirect.dll"}, 0x20b, 2, 128); + put16(incompleteEntry, PeOffset + 20, 127); + failsWithoutTouchingTheFile(directory, "optional-header-incomplete-import-entry", incompleteEntry, "the image has no imports"); auto unterminated = image({"kernel32.dll", "mimalloc-redirect.dll"}); unterminated.resize(SectionOffset + 3 * DescriptorBytes + 13 + 21); failsWithoutTouchingTheFile(directory, "unterminated-name", unterminated, "unterminated import name"); From 0701378a565c1fe1148fb322eba825c9d79fe711 Mon Sep 17 00:00:00 2001 From: Daniel Castrillon <112592276+danielcadev@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:05:40 -0500 Subject: [PATCH 496/702] feat(pad): inject touch coordinates through command files --- .../libScePad/include/InputCommandFile.hpp | 44 ++++++++++++--- core/libs/prx/libScePad/src/PadState.cpp | 10 +++- core/libs/tests/GuestPad.cpp | 53 ++++++++++++++++++- tools/pad_input.py | 13 ++++- tools/tests/test_pad_input.py | 31 ++++++++++- 5 files changed, 139 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index e372a1f3f7..172735fd62 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -11,6 +11,7 @@ #include <sstream> #include <filesystem> #include <string> +#include <string_view> #include <utility> #include <vector> @@ -23,6 +24,11 @@ public: std::array<std::uint8_t, 4> sticks{}; bool touchLeft = false; bool touchRight = false; + bool hasTouch = false; + std::uint16_t touchX = 0; + std::uint16_t touchY = 0; + std::uint8_t touchId = 0; + bool touchActive = false; bool operator==(const Input&) const = default; }; @@ -107,21 +113,37 @@ private: } fields.clear(); fields.str(text); - std::string tokens[8]; + std::string tokens[9]; int count = 0; - while (count < 8 && fields >> tokens[count]) ++count; + while (count < 9 && fields >> tokens[count]) ++count; + Input input; + if (count == 4 || count == 8) { + std::string_view touch(tokens[--count]); + if (!touch.starts_with("touch(") || !touch.ends_with(')')) return {}; + touch.remove_prefix(6); + touch.remove_suffix(1); + std::uint64_t coordinates[4]{}; + for (int i = 0; i < 4; ++i) { + const auto comma = touch.find(','); + if ((i < 3) != (comma != std::string_view::npos)) return {}; + if (!parseUnsigned(touch.substr(0, comma), coordinates[i])) return {}; + if (i < 3) touch.remove_prefix(comma + 1); + } + if (coordinates[0] >= 1920 || coordinates[1] >= 943 || coordinates[2] > 127 || coordinates[3] > 1) return {}; + input.hasTouch = true; + input.touchX = static_cast<std::uint16_t>(coordinates[0]); + input.touchY = static_cast<std::uint16_t>(coordinates[1]); + input.touchId = static_cast<std::uint8_t>(coordinates[2]); + input.touchActive = coordinates[3] != 0; + } 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 (!parseUnsigned(tokens[i], values[i])) return {}; } if (values[0] == 0 || (values[1] & ~(0x10fffeull | TouchLeftBit | TouchRightBit)) != 0 || values[2] == 0 || values[2] > 5000) return {}; Command command; command.sequence = values[0]; - Input input; input.buttons = static_cast<std::uint32_t>(values[1] & 0x10fffeull); input.touchLeft = (values[1] & TouchLeftBit) != 0; input.touchRight = (values[1] & TouchRightBit) != 0; @@ -134,6 +156,14 @@ private: return command; } + static bool parseUnsigned(std::string_view token, std::uint64_t& value) { + if (token.empty()) return false; + const char* first = token.data() + (token.front() == '+' ? 1 : 0); + const char* last = token.data() + token.size(); + const auto parsed = std::from_chars(first, last, value); + return first != last && parsed.ec == std::errc{} && parsed.ptr == last; + } + static constexpr std::uint64_t TouchLeftBit = 0x200000; static constexpr std::uint64_t TouchRightBit = 0x400000; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 0c685c42bc..2b57276eed 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -211,11 +211,19 @@ PadData BuildData(PadSlot& slot, const PadInputState& state, std::uint64_t chang data.timestamp = changeTime; std::uint8_t touchNum = 0; + const bool injectedContact = !intercepted && injected.hasTouch; + if (injectedContact && injected.touchActive) { + touchNum = 1; + data.touch_data_touch0_x = injected.touchX; + data.touch_data_touch0_y = injected.touchY; + data.touch_data_touch0_id = injected.touchId; + } for (int i = 0; i < 2; ++i) { const PadTouchPoint& tp = state.touch[i]; if (!tp.active) { slot.prevTouchActive[i] = false; continue; } if (!slot.prevTouchActive[i]) slot.touchIds[i] = static_cast<std::uint8_t>(slot.nextTouchId++ & 0x7F); slot.prevTouchActive[i] = true; + if (injectedContact) continue; if (touchNum == 0) { data.touch_data_touch0_x = tp.x; data.touch_data_touch0_y = tp.y; data.touch_data_touch0_id = slot.touchIds[i]; } else { @@ -224,7 +232,7 @@ PadData BuildData(PadSlot& slot, const PadInputState& state, std::uint64_t chang ++touchNum; } const bool injectedTouch = !intercepted && (injected.touchLeft || injected.touchRight); - if (touchNum == 0 && (state.touchLeft || state.touchRight || injectedTouch)) { + if (!injectedContact && touchNum == 0 && (state.touchLeft || state.touchRight || injectedTouch)) { data.buttons |= 0x100000; touchNum = 1; data.touch_data_touch0_x = state.touchRight || (!intercepted && injected.touchRight) ? kTouchPadWidth * 3 / 4 : kTouchPadWidth / 4; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index ca20015351..3f86b4ae82 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -79,8 +79,29 @@ static void CheckInputCommands() { Require(right.buttons == 0x4000 && !right.touchLeft && right.touchRight); write("10 8388608 300"); Require(commands.Poll(3801000) == none); + write("10 0 5000 touch(1919,942,127,1)"); + const auto contact = commands.Poll(3901000); + Require(contact.hasTouch && contact.touchActive && contact.touchX == 1919 && contact.touchY == 942 && contact.touchId == 127); + Require(contact.buttons == 0 && !contact.touchLeft && !contact.touchRight); + Require(commands.Poll(4001000) == contact); + write("11 0 5000 1 2 3 4 touch(0,0,127,1)"); + const auto moved = commands.Poll(4101000); + Require(moved.hasTouch && moved.touchActive && moved.touchX == 0 && moved.touchY == 0 && moved.touchId == contact.touchId); + Require(moved.hasSticks && moved.sticks[3] == 4); + write("12 0 5000 touch(0,0,127,0)"); + const auto lifted = commands.Poll(4201000); + Require(lifted.hasTouch && !lifted.touchActive && lifted.touchId == 127 && lifted.buttons == 0); + std::uint64_t poll = 4301000; + for (const char* invalid : {"touch(1920,0,0,1)", "touch(0,943,0,1)", "touch(0,0,128,1)", + "touch(0,0,0,2)", "touch(-1,0,0,1)", "touch(0,0,0)", "touch(0,0,0,1,1)", + "touch(0,,0,1)", "touch(0,0,0,1)x", "touch(0,0,0,1) trailing"}) { + std::ofstream(path) << "13 0 5000 " << invalid << '\n'; + Require(commands.Poll(poll) == lifted); + poll += 100000; + } + Require(commands.Poll(9201000) == none); std::filesystem::remove(path); - Require(commands.Buttons(4201000) == 0); + Require(commands.Buttons(9301000) == 0); Pad::InputCommandFile disabled(std::filesystem::path{}); Require(disabled.Buttons(0) == 0); } @@ -632,6 +653,34 @@ static void CheckInjectedState(const std::filesystem::path& path) { const auto lifted = Pad::ReadState(); Require(lifted.touch_data_touch_num == 0 && lifted.buttons == 0x2000); } + bool contactActive = false; + const auto touchCommand = [&](const char* command, int x, int y, int id, bool active) { + std::ofstream(path) << command << '\n'; + PadData touched{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + touched = Pad::ReadState(); + if (contactActive && active) Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_id == id); + if (touched.touch_data_touch_num == (active ? 1 : 0) && + (!active || (touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == y && touched.touch_data_touch0_id == id))) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } while (std::chrono::steady_clock::now() < deadline); + Require(touched.touch_data_touch_num == (active ? 1 : 0)); + if (active) Require(touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == y && touched.touch_data_touch0_id == id); + Require(touched.buttons == 0x2000); + contactActive = active; + return touched; + }; + const auto start = touchCommand("5 0 5000 touch(1919,942,127,1)", 1919, 942, 127, true); + const auto turn = touchCommand("6 0 5000 touch(0,942,127,1)", 0, 942, 127, true); + Require(turn.timestamp > start.timestamp); + const auto end = touchCommand("7 0 5000 touch(0,0,127,1)", 0, 0, 127, true); + Require(end.timestamp > turn.timestamp); + const auto lifted = touchCommand("8 0 300 touch(0,0,127,0)", 0, 0, 127, false); + Require(lifted.timestamp > end.timestamp); + touchCommand("9 0 150 touch(960,471,0,1)", 960, 471, 0, true); + std::this_thread::sleep_for(std::chrono::milliseconds(160)); + Require(Pad::ReadState().touch_data_touch_num == 0); PadPublishInput_nid_postfix(PadInputState{}); } @@ -642,7 +691,7 @@ static void CheckInjectedQueueTimes(const std::filesystem::path& path) { physical.buttons = 0x2000; PadPublishInput_nid_postfix(physical); const auto before = Pad::ReadState(); - std::ofstream(path) << "5 16384 300\n"; + std::ofstream(path) << "10 16384 300\n"; PadData pressed{}; const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); do { diff --git a/tools/pad_input.py b/tools/pad_input.py index 3ebe40ade0..a62f5f51d7 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -27,6 +27,10 @@ def previous_sequence(path): line = line[:-2] + b"\n" tokens = line.split() timeline = tokens[:1] == [b"APS5_PAD_TIMELINE"] + if not timeline and len(tokens) in (4, 8): + touch = re.fullmatch(rb"touch\((\+?[0-9]+),(\+?[0-9]+),(\+?[0-9]+),(\+?[0-9]+)\)", tokens.pop()) + if touch is None or any(int(value) > limit for value, limit in zip(touch.groups(), (1919, 942, 127, 1))): + return 0 if timeline: tokens = tokens[1:] if len(line.rstrip(b"\n")) > 127 or len(tokens) not in ((3,) if timeline else (3, 7)): @@ -69,6 +73,8 @@ def main(): 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")) + parser.add_argument("--touch", type=int, nargs=4, metavar=("X", "Y", "ID", "ACTIVE"), + help="touch contact: X 0..1919, Y 0..942, ID 0..127, ACTIVE 0 or 1; keep ID while moving, use 0 to lift") args = parser.parse_args() try: mask = button_mask(args.button) @@ -80,6 +86,11 @@ def main(): parser.error(f"directory does not exist: {args.path.parent}") if not 1 <= args.hold_ms <= 5000: parser.error("--hold-ms must be between 1 and 5000") + touch = "" + if args.touch is not None: + if any(not 0 <= value <= limit for value, limit in zip(args.touch, (1919, 942, 127, 1))): + parser.error("--touch requires X 0..1919, Y 0..942, ID 0..127, ACTIVE 0 or 1") + touch = " touch(" + ",".join(str(value) for value in args.touch) + ")" 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]) @@ -104,7 +115,7 @@ def main(): if os.geteuid() == 0: os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid) os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode)) - output.write(f"{sequence} {mask} {args.hold_ms}{sticks}\n") + output.write(f"{sequence} {mask} {args.hold_ms}{sticks}{touch}\n") os.replace(temporary, args.path) finally: if temporary is not None: diff --git a/tools/tests/test_pad_input.py b/tools/tests/test_pad_input.py index 872bdd58fe..12700c78e6 100644 --- a/tools/tests/test_pad_input.py +++ b/tools/tests/test_pad_input.py @@ -25,7 +25,8 @@ class PadInputTests(unittest.TestCase): mock.patch.object(pad_input.time, "monotonic_ns", return_value=clock), \ contextlib.redirect_stdout(io.StringIO()): pad_input.main() - return [int(token) for token in self.path.read_text(encoding="utf-8").split()] + return [token if token.startswith("touch(") else int(token) + for token in self.path.read_text(encoding="utf-8").split()] @unittest.skipUnless(os.name == "posix", "POSIX file permissions") def test_replacement_keeps_shared_command_mode(self): @@ -101,6 +102,34 @@ class PadInputTests(unittest.TestCase): self.assertIn("invalid button", errors.getvalue()) self.assertEqual(self.path.read_text(encoding="utf-8"), original) + def test_touch_coordinates_preserve_id_and_allow_explicit_lift(self): + self.assertEqual(self.queue(1000, "release", "--hold-ms", "5000", "--touch", "1919", "942", "127", "1"), + [1000, 0, 5000, "touch(1919,942,127,1)"]) + self.assertEqual(self.queue(1000, "release", "--touch", "0", "942", "127", "1"), + [1001, 0, 300, "touch(0,942,127,1)"]) + self.assertEqual(self.queue(1000, "release", "--hold-ms", "1", "--touch", "0", "0", "127", "0"), + [1002, 0, 1, "touch(0,0,127,0)"]) + self.assertEqual(self.queue(1000, "cross", "--left-stick", "0", "255", "--touch", "0", "0", "0", "1"), + [1003, 16384, 300, 0, 255, 128, 128, "touch(0,0,0,1)"]) + + def test_invalid_touch_arguments_preserve_command(self): + original = "2000 0 300 touch(100,200,3,1)\n" + for coordinates in [(1920, 0, 0, 1), (0, 943, 0, 1), (0, 0, 128, 1), (0, 0, 0, 2), (-1, 0, 0, 1)]: + with self.subTest(coordinates=coordinates): + self.path.write_text(original, encoding="utf-8") + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + self.queue(1000, "release", "--touch", *map(str, coordinates)) + self.assertEqual(error.exception.code, 2) + self.assertEqual(self.path.read_text(encoding="utf-8"), original) + + def test_invalid_touch_file_does_not_preserve_sequence(self): + for token in ["touch(1920,0,0,1)", "touch(0,943,0,1)", "touch(0,0,128,1)", "touch(0,0,0,2)", + "touch(-1,0,0,1)", "touch(0,0,0)", "touch(0,0,0,1,1)", "touch(0,,0,1)", "touch(0,0,0,1)x"]: + with self.subTest(token=token): + self.path.write_text(f"2000 0 300 {token}\n", encoding="utf-8") + self.assertEqual(self.queue(1000, "release"), [1000, 0, 300]) + + def test_valid_stick_command_and_unsigned_tokens_preserve_sequence(self): self.path.write_text("+2000 +16384 +300 +0 +255 +128 +128\n", encoding="utf-8") self.assertEqual(self.queue(1000, "release"), [2001, 0, 300]) From 78afe4c2b8c71f70f0801fd94514d82331da575c Mon Sep 17 00:00:00 2001 From: tkcm01 <75453983+tkcm01@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:51:39 -0400 Subject: [PATCH 497/702] fix(agc): read the process's private memory from task_info on macOS ReadProcessPrivateBytes reads RssAnon and VmSwap from /proc/self/status on every host other than Windows. macOS has no /proc, so the overlay's RAM line stays empty and agc_performance_overlay_statistics fails with "the process's private memory cannot be read". On macOS it now returns the task's phys_footprint from task_info(TASK_VM_INFO): its resident and compressed private memory, which is what Activity Monitor shows. jotitadev measured that this makes the test pass on macOS (#7886). The same change is part of the macOS port in #8658. Co-Authored-By: iamthehimansh <67262056+iamthehimansh@users.noreply.github.com> Co-Authored-By: jotitadev <139649225+jotitadev@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/Execution/src/PerformanceOverlay.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index ad19a30724..96c60dbd56 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -34,6 +34,9 @@ #include <psapi.h> #else #include <sys/resource.h> +#ifdef __APPLE__ +#include <mach/mach.h> +#endif #endif namespace AgcDriver { @@ -616,6 +619,11 @@ std::optional<std::uint64_t> ReadProcessPrivateBytes() { counters.cb = sizeof(counters); if (K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&counters), sizeof(counters)) == 0) return std::nullopt; return static_cast<std::uint64_t>(counters.PrivateUsage); +#elif defined(__APPLE__) + task_vm_info_data_t info{}; + mach_msg_type_number_t count = TASK_VM_INFO_COUNT; + if (task_info(mach_task_self(), TASK_VM_INFO, reinterpret_cast<task_info_t>(&info), &count) != KERN_SUCCESS || info.phys_footprint == 0) return std::nullopt; + return static_cast<std::uint64_t>(info.phys_footprint); #else std::FILE* status = std::fopen("/proc/self/status", "r"); if (status == nullptr) return std::nullopt; From 34c5ae23f864eaacd830e6e46f6e33662ff5cf61 Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:40:06 +0200 Subject: [PATCH 498/702] docs(libkernel): record PS5 results for two libkernel exports --- docs/dev/debt/libkernel.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0fe8091311..28eea6ea49 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -10,7 +10,7 @@ - [pthread_rwlockattr_settype_np](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - stores types 1 and 2 through `scePthreadRwlockattrSettype` and returns 0, but the type is not applied: `scePthreadRwlockInit` ignores its attribute. Any other type, 0 included, and a null attribute or object return `EINVAL` and store nothing, as in [BunnyFl's measurements](https://github.com/boykopovar/AnyPS5/pull/9632#issuecomment-6363402814) of firmware 13.42's libkernel. Only the name is public; the `(attr, int)` signature follows the SCE twin and shadPS4, which also registers it as `scePthreadRwlockattrSettype` - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, where a payload process on a CFI-2014 PS5 (firmware 13.60) got 0x840f80, 1 and 800000000. The SoC id may differ between console models, and the CPU mode of a title may differ from a payload's (shadPS4 takes the PS4's from the param.sfo flags), so neither is copied. `sceKernelGetMainSocId` ignores a pointer argument there, as here. PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`) - [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 -- [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - internal pool allocation and statistics are not implemented, so a non-null query throws without changing its output. Native null output returns -1; an absent pool returns -2, a failed statistics query returns -3, and success writes total pool bytes minus used bytes. +- [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - internal pool allocation and statistics are not implemented, so a non-null query throws without changing its output. A null output returns -1 here, as a payload process on a PS5 (firmware 12.70) got, with `errno` 0; for a valid output it got -2 with `errno` 0 and left the output untouched. Whether -2 means an absent pool, and what a failed statistics query or a success returns, is unmeasured, as is any title process - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) - the native zero-argument query returns bit 3 of the process sanitizer flags loaded from sysctl `{1, 14, 41}`. The host always reports disabled because guest sanitizer activation is not modeled. - [pthread_cond_setname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) - takes `(cond, name)` and returns 0 without keeping the name, as `scePthreadCondInit` also ignores its name and nothing reads one back; a statically initialized condition variable is created first, and a destroyed one returns `EINVAL`. FreeBSD 11.4 libthr has no such function; the signature comes only from shadPS4 ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/kernel/threads/condvar.cpp#L80-L88)), which stores the name, returns `EINVAL` for a null `name`, and returns 0 for a statically initialized condition variable without creating it. A null `cond` or a null `name` throws here, because the console's result for either was not measured. @@ -34,7 +34,7 @@ - [kern.osreldate, kern.osrevision, kern.argmax, kern.ngroups](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` (1.24, 1.3, 1.8, 1.18) and `sysctlbyname` report 900000, 199506, 262144 and 1023, the 4-byte values a payload process on a PS5 (firmware 13.60) read; a short buffer returns `ENOMEM` and a write `EPERM` - [kern.boottime](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_BOOTTIME`} and `sysctlbyname` report a read-only 16-byte `struct timeval`, as FreeBSD 11.4's [`sysctl_kern_boottime`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_tc.c#L144-L163) does: the guest's `CLOCK_REALTIME` minus its `CLOCK_UPTIME`, taken at the first read and kept, so a later step of the host clock does not move it as FreeBSD's `tc_setclock` would. A payload process on PS5 firmware 13.60 gets the same value, `CLOCK_REALTIME` minus `CLOCK_UPTIME`, unchanged 2 s later, and an 8-byte buffer gets the first 8 bytes and `ENOMEM`; what a title process gets has not been measured. - [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). Measured on PS5 firmware 13.60: every size (0, 0x27, 0x28, 0x29, 0x30, 0x100 and 2^64-1) returns 0 and is preserved, bytes 8 to 0x27 are written and nothing from 0x28 onward, and a null pointer returns 0 without a fault or an `errno` change -- [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 +- [sceKernelGetAllowedSdkVersionOnSystem](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports `0x01000fff` (the system software version with the 12 patch bits set), where a payload process on a PS5 (firmware 12.70) got 0 and wrote `0x12700fff` in 4 bytes: which matches the kernel firmware version 12.70 (`0x12700001`, as the payload SDK's `kernel_get_fw_version` reported it) with the 12 patch bits set, rather than the `13.090.001` that `sceKernelGetSystemSwVersion` reported in the same process. The `0x01000fff` pattern follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) on the PS4 and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Explorer (PPSA99109) reads it through. A null pointer returns `SCE_KERNEL_ERROR_EINVAL` (`0x80020016`) here and in that process. Other firmware and a title process are unmeasured - [sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) convert with the host time zone at the given instant, as libc `localtime` does, instead of the console's time zone setting. `west_sec` holds the standard-time offset in seconds east of UTC, despite its name, and `dst_sec` the daylight saving seconds in effect. This follows PPSA99203 ([PS5SX2](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34e8a2a1cd9302df4373e5a94d28f4/ps5/frontend/fe_ps5.cpp#L54-L63)), whose author saw a console write 7200 and 3600 into the 16-byte block; which zone that console was set to is not recorded, so whether `west_sec` includes daylight saving is not settled. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333/src/core/libraries/kernel/time.cpp) also stores seconds east. PPSA99203 declares the last argument of `sceKernelConvertUtcToLocaltime` as `uint64_t*`; 32 bits are written. On Windows the standard offset and the daylight saving amount are the C runtime's current `_get_timezone` and `_get_dstbias` for every instant, as libc `localtime_s` uses them, so instants under a zone's earlier rules get today's offsets; a `TZ` variable does not set `_dstbias`, which keeps the value an earlier `_tzset` read from the system (0 when the system zone has no daylight saving), and summer time then adds nothing. In a repeated or skipped local hour, `sceKernelConvertLocaltimeToUtc` takes the offset in effect one standard offset earlier. Its last argument receives the zone's daylight saving amount whether or not it is in effect, found by sampling the host zone over the year around the result, while `dst_sec` is the amount in effect: the libc `mktime` that PPSA99109 bundles (text 0x70ebb-0x70ee5) subtracts that amount when `tm_isdst` is set outside summer time. That `mktime` passes `tm_isdst` as the second argument, which is ignored, so it does not choose the offset in a repeated hour. The `gettimeofday` time zone still reports no offset and no daylight saving. A host time the C runtime cannot convert throws - [sceKernelConvertUtcToLocaltime and sceKernelConvertLocaltimeToUtc, measured](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) - a payload process on a PS5 (firmware 13.60) set to UTC-3 with Brazil's history differs from AnyPS5 in two ways. The 16-byte block's first 8 bytes hold the zone's last transition at or before the instant (`1550368800`, the end of summer time in February 2019, for every 2026 and 2038 date; `-57967200` for 1970), not the instant. The last argument is 3600 in every case but `sceKernelConvertLocaltimeToUtc` at 1970-01-01 (0), even in 2026, when the zone has had no summer time since 2019. The offsets, `dst_sec` and the converted times match the zone's history, and both calls return 0 with null outputs. Reproducing the transition needs the zone's full history, which the Windows C runtime does not have - [sceKernelGettimezone](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) reports the host zone, not the console's time zone setting: `tz_minuteswest` holds the standard offset in minutes east of UTC, despite its name, and `tz_dsttime` the zone's daylight saving amount in minutes, found as for the last argument of `sceKernelConvertLocaltimeToUtc`. A PS5 on firmware 13.40 set to US Eastern returned -300 and 60 (measured 2026-10-19, in summer time); what it returns outside summer time or for a zone without daylight saving was not measured. The Windows limits of the entry above apply. From 302288f9fc4173ff16f38f57213048eb244030cb Mon Sep 17 00:00:00 2001 From: Bram910 <bram@makowski.be> Date: Tue, 27 Oct 2026 13:11:16 +0100 Subject: [PATCH 499/702] fix(agc): recover pipeline cache persistence from transient failures --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 23 ++ .../Graphics/src/PipelineCache.cpp | 13 +- .../tests/PipelineCacheFailures.cpp | 238 ++++++++++++++++++ 3 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/PipelineCacheFailures.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 401e0a2b9d..b75a39f4bf 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1568,6 +1568,29 @@ add_custom_command( VERBATIM ) +add_test_executable(agc_pipeline_cache_allocation_tests EXCLUDE_FROM_ALL tests/PipelineCacheFailures.cpp + Graphics/src/PipelineCache.cpp tests/AllocationFailure.cpp) +target_include_directories(agc_pipeline_cache_allocation_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_pipeline_cache_allocation_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_pipeline_cache_allocation_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_pipeline_cache_allocation_tests) +if(BUILD_TESTING) + set_target_properties(agc_pipeline_cache_allocation_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(mode startup now busy destructor worker) + add_test(NAME agc_pipeline_cache_allocation_${mode} COMMAND agc_pipeline_cache_allocation_tests ${mode}) + set_tests_properties(agc_pipeline_cache_allocation_${mode} PROPERTIES TIMEOUT 25) + if(WIN32) + set_tests_properties(agc_pipeline_cache_allocation_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() + endforeach() + if(WIN32) + add_test(NAME agc_pipeline_cache_read_retry COMMAND agc_pipeline_cache_allocation_tests read-retry) + set_tests_properties(agc_pipeline_cache_read_retry PROPERTIES TIMEOUT 25 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() + add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL tests/Recorder.cpp tests/HostImportLifetime.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index 28df53956c..ab8802ea4d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -8,6 +8,7 @@ #include <cstring> #include <fstream> #include <map> +#include <new> #include <span> #include <string> #include <vector> @@ -72,7 +73,12 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope result = create(context.device, &info, nullptr, &cache); } Check(result, "vkCreatePipelineCache"); - if (!path.empty()) saver = std::thread([this] { run(); }); + try { + if (!path.empty()) saver = std::thread([this] { run(); }); + } catch (...) { + context.Function<PFN_vkDestroyPipelineCache>("vkDestroyPipelineCache")(context.device, cache, nullptr); + throw; + } } PipelineCache::~PipelineCache() { @@ -179,7 +185,7 @@ void PipelineCache::load(std::vector<std::byte>& initialData) { std::fprintf(stderr, "[pipeline-cache] loaded %.1f KiB from %s\n", static_cast<double>(initialData.size()) / 1024.0, path.string().c_str()); } -void PipelineCache::save(bool final, bool waitForPipelines) { +void PipelineCache::save(bool final, bool waitForPipelines) try { std::lock_guard pathLock(pathMutex(path)); const auto getData = context.Function<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData"); const auto snapshot = [&] { return waitForPipelines ? synchronization.LockForSnapshot() : synchronization.TryLockForSnapshot(); }; @@ -200,7 +206,6 @@ void PipelineCache::save(bool final, bool waitForPipelines) { auto disk = fileUnchanged ? File{} : readFile(); if (disk.state == FileState::Unreadable) { std::fprintf(stderr, "[pipeline-cache] cannot read %s; not saving over it\n", path.string().c_str()); - settle(readHeader()); return; } if (disk.state == FileState::Invalid) std::fprintf(stderr, "[pipeline-cache] replacing %s: %s\n", path.string().c_str(), disk.invalidReason); @@ -271,6 +276,8 @@ void PipelineCache::save(bool final, bool waitForPipelines) { } settle(header); if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s%s\n", static_cast<double>(size) / 1024.0, path.string().c_str(), std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count(), mergedWithFile ? " (merged with the file)" : "", final ? " (teardown)" : ""); +} catch (const std::bad_alloc&) { + std::fputs("[pipeline-cache] save skipped: host memory exhausted\n", stderr); } void PipelineCache::SaveNow() { diff --git a/core/libs/prx/libSceAgcDriver/tests/PipelineCacheFailures.cpp b/core/libs/prx/libSceAgcDriver/tests/PipelineCacheFailures.cpp new file mode 100644 index 0000000000..7e13f9881c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PipelineCacheFailures.cpp @@ -0,0 +1,238 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp" +#include "AllocationFailure.hpp" +#include "ShaderCacheDirectory.hpp" +#include <array> +#include <atomic> +#include <chrono> +#include <condition_variable> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <fstream> +#include <iterator> +#include <map> +#include <memory> +#include <mutex> +#include <new> +#include <random> +#include <string_view> + +using namespace AgcDriver::Graphics; +using namespace std::chrono_literals; + +namespace { + +void Check(bool value, const char* reason) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", reason); + std::fflush(stderr); + std::_Exit(1); +} + +std::mutex mutex; +std::condition_variable changed; +std::map<VkPipelineCache, std::vector<std::byte>> caches; +std::uintptr_t next = 1; +std::atomic<bool> armed{false}; +std::atomic<bool> failStartup{false}; +std::atomic<unsigned> queries{0}; +std::atomic<unsigned> faults{0}; +std::size_t initialBytes = 64; + +std::vector<std::byte> Payload() { + std::vector<std::byte> bytes(initialBytes); + const std::array<std::uint32_t, 4> header{32, VK_PIPELINE_CACHE_HEADER_VERSION_ONE, 13, 42}; + std::memcpy(bytes.data(), header.data(), sizeof(header)); + return bytes; +} + +VKAPI_ATTR VkResult VKAPI_CALL Create(VkDevice, const VkPipelineCacheCreateInfo* info, const VkAllocationCallbacks*, VkPipelineCache* output) { + std::lock_guard lock(mutex); + *output = reinterpret_cast<VkPipelineCache>(next++); + if (info->initialDataSize != 0) { + const auto* bytes = static_cast<const std::byte*>(info->pInitialData); + caches[*output] = std::vector<std::byte>(bytes, bytes + info->initialDataSize); + } else caches[*output] = Payload(); + if (failStartup.exchange(false)) { + ++faults; + AgcDriver::Tests::SetAllocationFailureCountdown(0); + } + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL Destroy(VkDevice, VkPipelineCache cache, const VkAllocationCallbacks*) { + std::lock_guard lock(mutex); + Check(caches.erase(cache) == 1, "destroyed an unknown cache"); + changed.notify_all(); +} + +VKAPI_ATTR VkResult VKAPI_CALL Merge(VkDevice, VkPipelineCache destination, std::uint32_t count, const VkPipelineCache* sources) { + std::lock_guard lock(mutex); + Check(count == 1, "expected one merge source"); + caches.at(destination) = caches.at(sources[0]); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL Data(VkDevice, VkPipelineCache cache, std::size_t* size, void* output) { + std::lock_guard lock(mutex); + const auto& bytes = caches.at(cache); + if (output != nullptr) { + Check(*size >= bytes.size(), "cache snapshot buffer is too small"); + std::memcpy(output, bytes.data(), bytes.size()); + } + *size = bytes.size(); + if (output == nullptr && armed && ++queries == 2) { + armed = false; + ++faults; + changed.notify_all(); + AgcDriver::Tests::SetAllocationFailureCountdown(0); + } + return VK_SUCCESS; +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice, const char* name) { + if (std::strcmp(name, "vkCreatePipelineCache") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Create); + if (std::strcmp(name, "vkDestroyPipelineCache") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Destroy); + if (std::strcmp(name, "vkGetPipelineCacheData") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Data); + if (std::strcmp(name, "vkMergePipelineCaches") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Merge); + return nullptr; +} + +std::vector<char> Read(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + Check(static_cast<bool>(file), "saved cache file is missing"); + return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()}; +} + +void Grow(VkPipelineCache cache, std::size_t bytes = 128) { + std::lock_guard lock(mutex); + caches.at(cache).resize(bytes, std::byte{9}); +} + +} + +int main(int argc, char** argv) { + Check(argc == 2, "expected startup, now, busy, destructor, worker or read-retry"); + const std::string_view mode(argv[1]); + Check(mode == "startup" || mode == "now" || mode == "busy" || mode == "destructor" || mode == "worker" || mode == "read-retry", "unknown mode"); +#ifndef _WIN32 + if (mode == "read-retry") return 77; +#endif + const auto root = std::filesystem::temp_directory_path() / ("aps5-cache-allocation-" + std::to_string(std::random_device{}())); + Check(std::filesystem::create_directory(root), "test cache directory already exists"); + struct Cleanup { + std::filesystem::path path; + ~Cleanup() { std::error_code error; std::filesystem::remove_all(path, error); } + } cleanup{root}; +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", root.wstring().c_str()); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + setenv("ANYPS5_SHADER_CACHE_DIR", root.string().c_str(), 1); +#endif + Context context{}; + context.device = reinterpret_cast<VkDevice>(std::uintptr_t{1}); + context.deviceProc = Resolve; + VkPhysicalDeviceProperties properties{}; + properties.vendorID = 13; + properties.deviceID = 42; + if (mode == "startup") { + failStartup = true; + bool failed = false; + try { + PipelineCache attempt(context, properties); + } catch (const std::bad_alloc&) { + failed = true; + } + Check(failed && faults == 1, "saver startup allocation failure was not propagated"); + Check(caches.empty(), "failed saver startup leaked the Vulkan pipeline cache"); + { + PipelineCache retry(context, properties); + Check(retry.Handle() != VK_NULL_HANDLE, "cache creation could not retry after failed saver startup"); + } + Check(caches.empty(), "successful retry leaked a Vulkan pipeline cache"); + std::puts("startup: propagated the allocation failure, released the cache and allowed recreation"); + return 0; + } + if (mode == "read-retry") initialBytes = 32768; + auto cache = std::make_unique<PipelineCache>(context, properties); + cache->SaveNow(); + std::filesystem::path file; + for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) if (entry.path().extension() == ".bin") file = entry.path(); + Check(!file.empty(), "initial save created no cache file"); +#ifdef _WIN32 + if (mode == "read-retry") { + { + PipelineCache writer(context, properties); + Grow(writer.Handle(), 40960); + writer.SaveNow(); + } + const auto before = Read(file); + Check(before.size() == 24 + 40960, "the second writer did not update the cache file"); + Grow(cache->Handle(), 65536); + std::error_code error; + const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(file, error); + Check(!error, "could not resolve the test cache path"); + struct TailLock { + HANDLE handle = INVALID_HANDLE_VALUE; + OVERLAPPED region{}; + void Release() { + if (handle == INVALID_HANDLE_VALUE) return; + Check(UnlockFileEx(handle, 0, 1, 0, ®ion) != 0, "could not unlock the cache body"); + CloseHandle(handle); + handle = INVALID_HANDLE_VALUE; + } + ~TailLock() { Release(); } + } locked; + locked.handle = CreateFileW(native.c_str(), GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Check(locked.handle != INVALID_HANDLE_VALUE, "could not open the cache body lock handle"); + locked.region.Offset = 16384; + Check(LockFileEx(locked.handle, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &locked.region) != 0, + "could not lock the cache body"); + { + std::ifstream header(file, std::ios::binary); + std::array<char, 24> bytes{}; + Check(static_cast<bool>(header.read(bytes.data(), bytes.size())), "the body lock unexpectedly blocked the header"); + } + std::vector<std::byte> unreadable; + Check(!ShaderRecompiler::ReadWholeFile(file, unreadable), "the body lock did not block a complete cache read"); + cache->SaveNow(); + locked.Release(); + Check(Read(file) == before, "a failed cache read overwrote the other writer's file"); + cache->SaveNow(); + Check(Read(file).size() == 24 + 65536, "a transient cache read failure prevented the later save"); + cache.reset(); + Check(caches.empty(), "cache read retry leaked Vulkan objects"); + std::puts("read-retry: kept the unreadable cache file and saved new pipelines after it became readable"); + return 0; + } +#endif + const auto original = Read(file); + Grow(cache->Handle()); + queries = 0; + armed = true; + if (mode == "destructor") cache.reset(); + else if (mode == "worker") { + std::unique_lock lock(mutex); + Check(changed.wait_for(lock, 14s, [] { return faults == 1; }), "background save did not reach the allocation failure"); + } else if (mode == "busy") cache->SaveUnlessBusy(); + else cache->SaveNow(); + Check(faults == 1 && !armed, "the snapshot allocation was not exercised"); + Check(Read(file) == original, "a failed snapshot replaced the previous cache file"); + { + std::unique_lock lock(mutex); + if (mode == "worker") Check(changed.wait_for(lock, 2s, [] { return caches.size() == 1; }), "background snapshot did not release its temporary cache"); + Check(caches.size() == (cache ? 1u : 0u), "failed snapshot leaked its temporary merged cache"); + } + if (!cache) { + cache = std::make_unique<PipelineCache>(context, properties); + Grow(cache->Handle()); + } + cache->SaveNow(); + Check(Read(file).size() == 24 + 128 && Read(file) != original, "a failed snapshot prevented a later successful save"); + cache.reset(); + Check(caches.empty(), "pipeline cache allocation test leaked Vulkan objects"); + std::printf("%.*s: allocation failure kept the old file, released temporary caches and allowed a retry\n", static_cast<int>(mode.size()), mode.data()); +} From c2bf2c595e267e74e67a0c2af5488ce7464dacda Mon Sep 17 00:00:00 2001 From: Bram910 <bram@makowski.be> Date: Tue, 27 Oct 2026 13:11:19 +0100 Subject: [PATCH 500/702] fix(shader): preserve ownership of mapped shader pack entries --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 63 +++++++++++++++++++ core/shader/recompiler/ShaderCachePack.cpp | 19 +++--- core/shader/recompiler/ShaderCachePack.hpp | 3 +- 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 401e0a2b9d..5c0e6f6a66 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2780,6 +2780,11 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test(NAME agc_shader_disk_cache_pack_ownership COMMAND agc_shader_disk_cache_tests --prebuilt-pack-ownership) + set_tests_properties(agc_shader_disk_cache_pack_ownership PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_shader_disk_cache_pack_ownership PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file) if(Python3_Interpreter_FOUND) set(housekeepingModes plain) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index d30a761388..9e4fac5c56 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -526,6 +526,62 @@ std::vector<std::byte> packBytes(const std::vector<std::pair<std::vector<std::by return bytes; } +void verifyPrebuiltPackOwnership(const std::filesystem::path& directory) { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + const auto path = directory / "ownership.pack"; + require(WriteFileAtomically(path, packBytes({{key, EncodeEntry(key, variant)}})), "cannot write the ownership test pack"); + CompiledVariant first, second; + PackLoadTimings timings; + const std::uint32_t* words = nullptr; + { + CachePack pack(path); + require(pack.Valid(), "the ownership test pack is invalid"); + require(pack.Load(key, first, timings) == LoadStatus::Loaded, "cannot load the first packed variant"); + require(pack.Load(key, second, timings) == LoadStatus::Loaded, "cannot load the second packed variant"); + words = first.artifact.spirv.Words().data(); + require(!first.artifact.spirv.empty() && words == second.artifact.spirv.Words().data(), "packed SPIR-V was copied instead of sharing its mapping"); + } + requireSameVariant(variant, first, "packed variant outlives its pack"); +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION region{}; + require(VirtualQuery(words, ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, "packed SPIR-V lost its mapping after pack destruction"); +#endif + auto edited = first.artifact.spirv; + edited[0] ^= 1u; + require(edited.Words().data() != words && edited.Words()[0] != first.artifact.spirv.Words()[0], "editing packed SPIR-V modified the shared mapping"); + first = {}; + requireSameVariant(variant, second, "last packed variant retains its mapping"); + std::vector<std::byte> published; + require(ReadWholeFile(path, published) && published == packBytes({{key, EncodeEntry(key, variant)}}), "editing packed SPIR-V modified the cache file"); + second = {}; +#ifdef _WIN32 + require(VirtualQuery(words, ®ion, sizeof(region)) == sizeof(region) && region.State == MEM_FREE, "the final packed variant did not release its mapping"); +#endif + const auto offset = sizeof(PackHeader) + sizeof(PackRecord); + published.insert(published.begin() + offset, std::byte{}); + PackHeader header; + PackRecord record; + std::memcpy(&header, published.data(), sizeof(header)); + std::memcpy(&record, published.data() + sizeof(header), sizeof(record)); + ++header.fileBytes; + ++record.offset; + std::memcpy(published.data() + sizeof(header), &record, sizeof(record)); + header.indexHash = ShaderRecompiler::HashBytes(std::span<const std::byte>(published).subspan(sizeof(header), sizeof(record))); + std::memcpy(published.data(), &header, sizeof(header)); + require(WriteFileAtomically(path, published), "cannot publish an unaligned packed entry"); + { + CachePack pack(path); + require(pack.Valid(), "an unaligned packed entry was rejected"); + require(pack.Load(key, first, timings) == LoadStatus::Loaded && pack.Load(key, second, timings) == LoadStatus::Loaded, "an unaligned packed entry did not load"); + require(first.artifact.spirv.Words().data() != second.artifact.spirv.Words().data(), "an unaligned packed module did not use aligned owned storage"); + } + requireSameVariant(variant, first, "unaligned packed variant outlives its pack"); + requireSameVariant(variant, second, "second unaligned packed variant outlives its pack"); +} + void refreshIndexHash(std::vector<std::byte>& pack, std::size_t entries) { using namespace ShaderRecompiler::ShaderDiskCache; const auto index = std::span(pack).subspan(sizeof(PackHeader), entries * sizeof(PackRecord)); @@ -2541,6 +2597,13 @@ int main(int argc, char** argv) { std::cout << "housekeeping version " << directory.filename().string() << '\n'; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--prebuilt-pack-ownership") { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-pack-ownership-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(directory); + verifyPrebuiltPackOwnership(directory); + std::filesystem::remove_all(directory); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--resource-plan-codec") { verifyResultRoundTrip(); verifyEntryRoundTrip(); diff --git a/core/shader/recompiler/ShaderCachePack.cpp b/core/shader/recompiler/ShaderCachePack.cpp index 6b15865823..31af167e95 100644 --- a/core/shader/recompiler/ShaderCachePack.cpp +++ b/core/shader/recompiler/ShaderCachePack.cpp @@ -49,11 +49,13 @@ CachePack::CachePack(const std::filesystem::path& path) { if (!GetFileSizeEx(handle, &size) || size.QuadPart < static_cast<LONGLONG>(sizeof(PackHeader)) || static_cast<std::uint64_t>(size.QuadPart) > std::numeric_limits<std::size_t>::max()) { CloseHandle(handle); error = "invalid size"; return; } - mapping = CreateFileMappingW(handle, nullptr, PAGE_READONLY, 0, 0, nullptr); - fileHandle = handle; + const auto mapping = CreateFileMappingW(handle, nullptr, PAGE_READONLY, 0, 0, nullptr); + CloseHandle(handle); if (!mapping) { error = "cannot map"; return; } const auto* data = static_cast<const std::byte*>(MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0)); + CloseHandle(mapping); if (!data) { error = "cannot map view"; return; } + owner = std::shared_ptr<const void>(data, [](const void* view) { UnmapViewOfFile(view); }); file = {data, static_cast<std::size_t>(size.QuadPart)}; #else const auto fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); @@ -65,20 +67,13 @@ CachePack::CachePack(const std::filesystem::path& path) { auto* data = mmap(nullptr, size.st_size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); if (data == MAP_FAILED) { error = "cannot map"; return; } + owner = std::shared_ptr<const void>(data, [bytes = static_cast<std::size_t>(size.st_size)](const void* view) { munmap(const_cast<void*>(view), bytes); }); file = {static_cast<const std::byte*>(data), static_cast<std::size_t>(size.st_size)}; #endif Validate(); } -CachePack::~CachePack() { -#ifdef _WIN32 - if (!file.empty()) UnmapViewOfFile(file.data()); - if (mapping) CloseHandle(mapping); - if (fileHandle) CloseHandle(fileHandle); -#else - if (!file.empty()) munmap(const_cast<std::byte*>(file.data()), file.size()); -#endif -} +CachePack::~CachePack() = default; void CachePack::Validate() { PackHeader header; @@ -107,7 +102,7 @@ LoadStatus CachePack::Load(std::span<const std::byte> key, CompiledVariant& vari for (auto it = first; it != records.end() && it->hash == hash; ++it) { const auto data = file.subspan(it->offset, it->size); const auto decode = Clock::now(); - const auto result = DecodeEntry(data, key, variant); + const auto result = DecodeEntry(data, key, variant, owner); timings.decodeNanoseconds += elapsed(decode); timings.bytes += data.size(); if (result == LoadStatus::Loaded) return result; diff --git a/core/shader/recompiler/ShaderCachePack.hpp b/core/shader/recompiler/ShaderCachePack.hpp index 24aff33610..c94875ad11 100644 --- a/core/shader/recompiler/ShaderCachePack.hpp +++ b/core/shader/recompiler/ShaderCachePack.hpp @@ -44,8 +44,7 @@ private: std::span<const std::byte> file; std::span<const PackRecord> records; std::string error; - void* mapping = nullptr; - void* fileHandle = nullptr; + std::shared_ptr<const void> owner; }; } From 882dc6e07dc1d9a00a68420acd7ff4f5df66ec79 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 13:23:23 +0100 Subject: [PATCH 501/702] test(libSceCamera): cover non-positive handle bounds in GuestCamera Signed-off-by: PrivacyOnion <granfili074@gmail.com> --- core/libs/tests/GuestCamera.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestCamera.cpp b/core/libs/tests/GuestCamera.cpp index 06c5b3b7ee..18afa7bc26 100644 --- a/core/libs/tests/GuestCamera.cpp +++ b/core/libs/tests/GuestCamera.cpp @@ -39,6 +39,15 @@ void CheckClosed() { const CameraStartParameter start = Start(1, 1); CameraFrameData frame{}; frame.sizeThis = sizeof(CameraFrameData); + for (const int handle : {0, -1}) { + Require(sceCameraClose(handle) == ErrorParam, "closing non-positive handle must report PARAM"); + Require(sceCameraCloseByHandle(handle) == ErrorParam, "closing by non-positive handle must report PARAM"); + Require(sceCameraStart(handle, &start) == ErrorParam, "starting non-positive handle must report PARAM"); + Require(sceCameraStartByHandle(handle, &start) == ErrorParam, "starting by non-positive handle must report PARAM"); + Require(sceCameraStop(handle) == ErrorParam, "stopping non-positive handle must report PARAM"); + Require(sceCameraStopByHandle(handle) == ErrorParam, "stopping by non-positive handle must report PARAM"); + Require(sceCameraIsValidFrameData(handle, &frame) == ErrorParam, "validating non-positive handle must report PARAM"); + } Require(sceCameraClose(1) == ErrorNotOpen, "closing before any open must report NOT_OPEN"); Require(sceCameraCloseByHandle(1) == ErrorNotOpen, "closing by handle before any open must report NOT_OPEN"); Require(sceCameraStart(1, &start) == ErrorNotOpen, "starting before any open must report NOT_OPEN"); @@ -99,6 +108,11 @@ void CheckFrames() { void CheckClose() { Require(sceCameraClose(0) == ErrorParam, "closing handle 0 must be rejected"); + Require(sceCameraClose(-1) == ErrorParam, "closing negative handle must be rejected"); + Require(sceCameraCloseByHandle(0) == ErrorParam, "closing by handle 0 must be rejected"); + Require(sceCameraCloseByHandle(-1) == ErrorParam, "closing by negative handle must be rejected"); + Require(sceCameraStopByHandle(0) == ErrorParam, "stopping by handle 0 must be rejected"); + Require(sceCameraStopByHandle(-1) == ErrorParam, "stopping by negative handle must be rejected"); const CameraStartParameter start = Start(1, 1); CameraFrameData frame{}; frame.sizeThis = sizeof(frame); From 654a905ad67b4b3050a50678e27c658f44dac640 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 13:23:32 +0100 Subject: [PATCH 502/702] test(libSceConvertKeycode): cover null character and keycode bounds in GuestConvertKeycode Signed-off-by: PrivacyOnion <granfili074@gmail.com> --- core/libs/tests/GuestConvertKeycodeTitle.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestConvertKeycodeTitle.cpp b/core/libs/tests/GuestConvertKeycodeTitle.cpp index 21ccc29fa5..0ee595de20 100644 --- a/core/libs/tests/GuestConvertKeycodeTitle.cpp +++ b/core/libs/tests/GuestConvertKeycodeTitle.cpp @@ -22,7 +22,10 @@ int main() { char16_t character = 0; if (sceConvertKeycodeGetVirtualKeycode(0, 0, nullptr) != static_cast<int>(0x80BC0031u)) return 1; if (sceConvertKeycodeGetVirtualKeycode(0, 0, &virtualKey) != static_cast<int>(0x80BC001Du)) return 1; + if (sceConvertKeycodeGetVirtualKeycode(0, 40, &virtualKey) != static_cast<int>(0x80BC001Du)) return 1; if (sceConvertKeycodeGetCharacter(0, 0, 0, nullptr) != static_cast<int>(0x80BC0031u)) return 1; if (sceConvertKeycodeGetCharacter(0, 0, 0, &character) != static_cast<int>(0x80BC001Du)) return 1; + if (sceConvertKeycodeGetCharacter(0, 0, 40, &character) != static_cast<int>(0x80BC001Du)) return 1; + if (sceConvertKeycodeGetImeKeyboardType(0x10000000, nullptr) != static_cast<int>(0x80BC0031u)) return 1; return sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4 ? 0 : 1; } From 5be476d44247eee49ed6c990c4e611a3c4b46bda Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 13:23:42 +0100 Subject: [PATCH 503/702] test(libSceNpMatching2): cover null argument validation in GuestNpMatching2Offline Signed-off-by: PrivacyOnion <granfili074@gmail.com> --- core/libs/tests/GuestNpMatching2Offline.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestNpMatching2Offline.cpp b/core/libs/tests/GuestNpMatching2Offline.cpp index 796c217bdf..2838e9fd73 100644 --- a/core/libs/tests/GuestNpMatching2Offline.cpp +++ b/core/libs/tests/GuestNpMatching2Offline.cpp @@ -10,6 +10,7 @@ namespace { constexpr int ResolverTimedOut = static_cast<int>(0x804101E2u); +constexpr int AlreadyInitialized = static_cast<int>(0x80550C02u); constexpr int NotInitialized = static_cast<int>(0x80550C03u); constexpr int ContextMax = static_cast<int>(0x80550C04u); constexpr int ContextNotStarted = static_cast<int>(0x80550C08u); @@ -42,9 +43,14 @@ int main() { Require(sceNpMatching2RegisterRoomMessageCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterRoomMessageCallback", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2RegisterSignalingCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterSignalingCallback", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2SetDefaultRequestOptParam(1, nullptr) == NotInitialized, "sceNpMatching2SetDefaultRequestOptParam", "a call before initialization did not return NOT_INITIALIZED first"); + Require(sceNpMatching2Terminate() == NotInitialized, "sceNpMatching2Terminate", "terminating before initialization did not return NOT_INITIALIZED"); + Require(sceNpMatching2Initialize(nullptr) == InvalidArgument, "sceNpMatching2Initialize", "a null parameter did not return INVALID_ARGUMENT"); + init.size = 0x10; + Require(sceNpMatching2Initialize(&init) == InvalidArgument, "sceNpMatching2Initialize", "an invalid parameter size did not return INVALID_ARGUMENT"); init.size = 0x30; Require(sceNpMatching2Initialize(&init) == 0, "sceNpMatching2Initialize", "initialization failed"); + Require(sceNpMatching2Initialize(&init) == AlreadyInitialized, "sceNpMatching2Initialize", "initializing when already initialized did not return ALREADY_INITIALIZED"); Require(sceNpMatching2CreateContext(nullptr, &context) == InvalidArgument, "sceNpMatching2CreateContext", "a null parameter was taken"); create.size = 0x30; @@ -80,11 +86,13 @@ int main() { Require(sceNpMatching2SetDefaultRequestOptParam(1, requestOpt) == 0, "sceNpMatching2SetDefaultRequestOptParam", "setting the default option failed"); Require(sceNpMatching2Terminate() == 0, "sceNpMatching2Terminate", "termination failed"); + Require(sceNpMatching2Terminate() == NotInitialized, "sceNpMatching2Terminate", "terminating after termination did not return NOT_INITIALIZED"); Require(sceNpMatching2DestroyContext(1) == NotInitialized, "sceNpMatching2DestroyContext", "a context was destroyed after termination"); Require(sceNpMatching2Initialize(&init) == 0, "sceNpMatching2Initialize", "initializing again failed"); Require(sceNpMatching2RegisterRoomEventCallback(5, nullptr, nullptr) == InvalidContextId, "sceNpMatching2Terminate", "a context created before termination is still found"); Require(sceNpMatching2CreateContext(&create, &created) == 0 && created == 1, "sceNpMatching2Terminate", "termination did not restart the context ids at 1"); Require(sceNpMatching2Terminate() == 0, "sceNpMatching2Terminate", "the second termination failed"); + Require(sceNpMatching2Terminate() == NotInitialized, "sceNpMatching2Terminate", "the third termination did not return NOT_INITIALIZED"); if (failures != 0) return 1; std::puts("NpMatching2 offline tests passed"); From c648e06c6157ee63eb9c1c280d092b95267326c6 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 13:23:51 +0100 Subject: [PATCH 504/702] test(libSceNpManager): cover argument bounds and state validation in GuestNpReachability Signed-off-by: PrivacyOnion <granfili074@gmail.com> --- core/libs/tests/GuestNpReachability.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestNpReachability.cpp b/core/libs/tests/GuestNpReachability.cpp index 1c16e32fe7..b2a400757a 100644 --- a/core/libs/tests/GuestNpReachability.cpp +++ b/core/libs/tests/GuestNpReachability.cpp @@ -6,6 +6,7 @@ static constexpr int ErrorArgument = static_cast<int>(0x80550003); static constexpr int ErrorAlreadyRegistered = static_cast<int>(0x80550008); static constexpr int ErrorNotRegistered = static_cast<int>(0x80550009); static constexpr int ErrorUserNotFound = static_cast<int>(0x80550007); +static constexpr int ErrorInvalidId = static_cast<int>(0x80550015); static void Require(bool value) { if (!value) std::abort(); } static void APS5_VABI Callback(int, uint32_t, void*) { std::abort(); } @@ -19,13 +20,31 @@ int main() { Require(sceNpRegisterNpReachabilityStateCallback(nullptr, nullptr) == ErrorArgument); uint32_t state = 99; Require(sceNpGetNpReachabilityState(0, &state) == 0 && state == 0); + Require(sceNpGetNpReachabilityState(0, nullptr) == ErrorArgument); Require(sceNpCheckCallback() == 0); Require(sceNpUnregisterNpReachabilityStateCallback() == 0); Require(sceNpUnregisterNpReachabilityStateCallback() == ErrorNotRegistered); Require(sceNpRegisterNpReachabilityStateCallback(callback, nullptr) == 0); Require(sceNpUnregisterNpReachabilityStateCallback() == 0); + Require(sceNpRegisterPlusEventCallback(nullptr, nullptr) == ErrorArgument); + Require(sceNpRegisterPremiumEventCallback(nullptr, nullptr) == ErrorArgument); + Require(sceNpRegisterStateCallback(nullptr, nullptr) == ErrorArgument); + Require(sceNpUnregisterStateCallback() == ErrorNotRegistered); + Require(sceNpRegisterStateCallbackA(nullptr, nullptr) == ErrorArgument); + Require(sceNpUnregisterStateCallbackA(0) == ErrorInvalidId); + Require(sceNpUnregisterStateCallbackA(99) == ErrorInvalidId); + int targetUser = 0; + Require(sceNpGetUserIdByAccountId(0, &targetUser) == ErrorArgument); + Require(sceNpGetUserIdByAccountId(12345, nullptr) == ErrorArgument); + bool isPlus = true; + Require(sceNpIsPlusMember(0, nullptr) == ErrorArgument); + Require(sceNpIsPlusMember(0, &isPlus) == 0 && !isPlus); NpOnlineId onlineId{}; onlineId.data[0] = 'p'; + uint32_t presence = 99; + Require(sceNpGetGamePresenceStatus(nullptr, &presence) == ErrorArgument); + Require(sceNpGetGamePresenceStatus(&onlineId, nullptr) == ErrorArgument); + Require(sceNpGetGamePresenceStatus(&onlineId, &presence) == 0 && presence == 0); Require(sceNpCheckNpAvailability(1, &onlineId, nullptr) == ErrorUserNotFound); Require(sceNpCheckNpAvailability(1, nullptr, nullptr) == ErrorArgument); std::uint8_t available[3]{0x55, 0x55, 0x55}; @@ -35,5 +54,5 @@ int main() { Require(available[1] == 0x55); Require(sceNpCheckNpAvailability(1, &onlineId, &available[1]) == ErrorUserNotFound); Require(available[0] == 0x55 && available[1] == 0 && available[2] == 0x55); - + return 0; } From 25e07ea713e5f05615d6a25da0c100c9850a7840 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 13:24:09 +0100 Subject: [PATCH 505/702] test(libSceContentSearch): cover invalid metadata and query bounds in GuestContentSearch Signed-off-by: PrivacyOnion <granfili074@gmail.com> --- core/libs/tests/GuestContentSearch.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp index bac0286fe0..1adc3da4a6 100644 --- a/core/libs/tests/GuestContentSearch.cpp +++ b/core/libs/tests/GuestContentSearch.cpp @@ -54,6 +54,8 @@ int main() { Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, &updateId) == ErrorNotInitialized); Require(sceContentSearchGetTotalContentSize(&columnSet, 1, &size, &updateId) == ErrorNotInitialized); Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, infos, &updateId) == ErrorNotInitialized); + Require(sceContentSearchGetMetadataFieldInfo(0, nullptr, nullptr, nullptr) == ErrorNotInitialized); + Require(sceContentSearchGetMetadataValue(0, nullptr, nullptr) == ErrorNotInitialized); Require(sceContentSearchOpenMetadata("/data/photo.jpg", &metadataId) == ErrorNotInitialized); Require(sceContentSearchOpenMetadataByContentId(1, &metadataId) == ErrorNotInitialized); Require(sceContentSearchCloseMetadata(1) == ErrorNotInitialized); @@ -95,6 +97,18 @@ int main() { Require(sceContentSearchGetNumOfContent(&columnSet, 1, nullptr, &updateId) == ErrorInvalidArgument); Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, nullptr) == ErrorInvalidArgument); Require(sceContentSearchGetTotalContentSize(nullptr, 1, &size, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchGetTotalContentSize(nullptr, 0, nullptr, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchGetTotalContentSize(nullptr, 0, &size, nullptr) == ErrorInvalidArgument); + std::int32_t metadataType = 0, fieldSize = 0; + Require(sceContentSearchGetMetadataFieldInfo(1, nullptr, nullptr, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchGetMetadataFieldInfo(1, "field", nullptr, &fieldSize) == ErrorInvalidArgument); + Require(sceContentSearchGetMetadataFieldInfo(1, "field", &metadataType, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchGetMetadataValue(1, nullptr, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchGetMetadataValue(1, "field", nullptr) == ErrorInvalidArgument); + Require(sceContentSearchOpenMetadata(nullptr, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchOpenMetadata("/data/photo.jpg", nullptr) == ErrorInvalidArgument); + Require(sceContentSearchOpenMetadata(nullptr, &metadataId) == ErrorInvalidArgument); + Require(sceContentSearchOpenMetadataByContentId(1, nullptr) == ErrorInvalidArgument); Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, nullptr, &updateId) == ErrorInvalidArgument); ContentSearchContentColumn unknownColumn{8, 0, &photo, 2}; ContentSearchContentColumnSet unknownColumnSet{&unknownColumn, 1, 2}; From 2cb3965b374eba6760b48ba117941f3d0e3f19a1 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 13:24:28 +0100 Subject: [PATCH 506/702] test(libSceNpSessionSignaling): cover context validation and invalid handles in GuestNpSessionSignaling Signed-off-by: PrivacyOnion <granfili074@gmail.com> --- core/libs/tests/GuestNpSessionSignaling.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index 1cd76c46ca..d69593f7e5 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -145,12 +145,16 @@ int main() { Require(sceNpSessionSignalingTerminate() == 0, "terminate failed"); Require(sceNpSessionSignalingDestroyContext(contextId) == NotInitialized, "destroy after terminate"); Require(sceNpSessionSignalingActivateUser(contextId, nullptr, nullptr) == NotInitialized, "activate user after terminate"); + Require(sceNpSessionSignalingDeactivate(contextId, 1) == NotInitialized, "deactivate after terminate"); Require(sceNpSessionSignalingGetConnectionInfo(contextId, 1, 1, nullptr) == NotInitialized, "connection info after terminate"); + Require(sceNpSessionSignalingGetConnectionStatus(contextId, 1, nullptr, nullptr, nullptr) == NotInitialized, "connection status after terminate"); + Require(sceNpSessionSignalingGetLocalNetInfo(contextId, nullptr) == NotInitialized, "local net info after terminate"); Require(sceNpSessionSignalingGetGroupInfo(netContextId, 1, nullptr) == NotInitialized, "group info after terminate"); Require(sceNpSessionSignalingGetGroupFromSessionId(netContextId, nullptr, nullptr) == NotInitialized, "group from session id after terminate"); Require(sceNpSessionSignalingGetConnectionFromNetAddress(netContextId, 0, 0, nullptr) == NotInitialized, "connection from net address after terminate"); Require(sceNpSessionSignalingGetConnectionFromNetAddress2(netContextId, 1, 0, 0, nullptr) == NotInitialized, "connection from group net address after terminate"); Require(sceNpSessionSignalingGetConnectionFromPeerAddress(netContextId, nullptr, nullptr) == NotInitialized, "connection from peer address after terminate"); + Require(sceNpSessionSignalingGetConnectionFromPeerAddress2(netContextId, 1, nullptr, nullptr) == NotInitialized, "connection from peer address 2 after terminate"); Require(sceNpSessionSignalingActivateSession(contextId, nullptr, 0, nullptr, nullptr) == NotInitialized, "activate after terminate"); Require(sceNpSessionSignalingTerminate() == NotInitialized, "repeated terminate"); Require(sceNpSessionSignalingInitialize(reinterpret_cast<const SessionSignalingInitParam*>(initializeParam)) == 0, "reinitialize"); From 0b772bf50e17660490279099630bb8151280fec4 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 13:24:57 +0100 Subject: [PATCH 507/702] docs(debt): record silent stubs in libScePlayerInvitationDialog debt Signed-off-by: PrivacyOnion <granfili074@gmail.com> --- docs/dev/debt/libScePlayerInvitationDialog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/debt/libScePlayerInvitationDialog.md b/docs/dev/debt/libScePlayerInvitationDialog.md index 4e249f6289..464662bb92 100644 --- a/docs/dev/debt/libScePlayerInvitationDialog.md +++ b/docs/dev/debt/libScePlayerInvitationDialog.md @@ -1,5 +1,9 @@ # libScePlayerInvitationDialog technical debt +### Silent stubs + +- [scePlayerInvitationDialogTerminate and scePlayerInvitationDialogUpdateStatus](../../../core/libs/prx/libScePlayerInvitationDialog/Export.cpp) (libScePlayerInvitationDialog) - dialog termination and status update return 0 without displaying UI, matching the simulated dialog completion model so titles proceed without blocking on user interaction. + ### Functional - [libScePlayerInvitationDialog](../../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. From 60941da8cafde1d2305628614a4d7aded8cab691 Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Tue, 27 Oct 2026 12:28:34 +0000 Subject: [PATCH 508/702] feat(libSceHttp2): declare eight additional exports --- core/libs/prx/libSceHttp2/CMakeLists.txt | 2 +- .../libs/prx/libSceHttp2/InventoryExports.cpp | 38 +++++++++++++++++++ .../libSceHttp2/include/InventoryExports.hpp | 16 ++++++++ core/libs/prx/libSceHttp2/libSceHttp2.hpp | 1 + docs/dev/debt/libSceHttp2.md | 13 +++++++ 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceHttp2/InventoryExports.cpp create mode 100644 core/libs/prx/libSceHttp2/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt index 94c68154da..4dfffb6175 100644 --- a/core/libs/prx/libSceHttp2/CMakeLists.txt +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -22,7 +22,7 @@ set_target_properties(anyps5_http2_transport PROPERTIES POSITION_INDEPENDENT_COD if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(anyps5_http2_transport INTERFACE LINKER:--exclude-libs,libanyps5_http2_transport.a) endif() -add_library(libSceHttp2 SHARED EXCLUDE_FROM_ALL Export.cpp Cookies.cpp) +add_library(libSceHttp2 SHARED EXCLUDE_FROM_ALL Export.cpp Cookies.cpp InventoryExports.cpp) target_include_directories(libSceHttp2 PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceHttp2 PRIVATE libc libkernel anyps5_http2_transport) if(WIN32) diff --git a/core/libs/prx/libSceHttp2/InventoryExports.cpp b/core/libs/prx/libSceHttp2/InventoryExports.cpp new file mode 100644 index 0000000000..f39f53289e --- /dev/null +++ b/core/libs/prx/libSceHttp2/InventoryExports.cpp @@ -0,0 +1,38 @@ +#include "prx/libSceHttp2/include/InventoryExports.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceHttp2GetAllTrailingHeaders() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceHttp2SetProxyWithURL() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceHttp2SetRequestServerName() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceHttp2SetStripGZIPHeadersEnabled() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceHttp2WebSocketClose() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceHttp2WebSocketSendDataMessage() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceHttp2WebSocketSendTextMessage() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceHttp2WebSocketSetCloseTimeout() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceHttp2/include/InventoryExports.hpp b/core/libs/prx/libSceHttp2/include/InventoryExports.hpp new file mode 100644 index 0000000000..7666182edb --- /dev/null +++ b/core/libs/prx/libSceHttp2/include/InventoryExports.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceHttp2GetAllTrailingHeaders(); +int APS5_VABI sceHttp2SetProxyWithURL(); +int APS5_VABI sceHttp2SetRequestServerName(); +int APS5_VABI sceHttp2SetStripGZIPHeadersEnabled(); +int APS5_VABI sceHttp2WebSocketClose(); +int APS5_VABI sceHttp2WebSocketSendDataMessage(); +int APS5_VABI sceHttp2WebSocketSendTextMessage(); +int APS5_VABI sceHttp2WebSocketSetCloseTimeout(); + +} diff --git a/core/libs/prx/libSceHttp2/libSceHttp2.hpp b/core/libs/prx/libSceHttp2/libSceHttp2.hpp index d6ec538a09..68e4c08f71 100644 --- a/core/libs/prx/libSceHttp2/libSceHttp2.hpp +++ b/core/libs/prx/libSceHttp2/libSceHttp2.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceHttp2/include/Http2.hpp" +#include "prx/libSceHttp2/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index f89a6dac77..e31d967b91 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -6,6 +6,19 @@ ### Unknown function info +- [Additional HTTP2 exports](../../../core/libs/prx/libSceHttp2/InventoryExports.cpp) - these name/NID pairs are present in the 13.60 export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +| Name | NID | +| --- | --- | +| `sceHttp2GetAllTrailingHeaders` | `CjxqMyx2-pU` | +| `sceHttp2SetProxyWithURL` | `fkKvmzFNBe8` | +| `sceHttp2SetRequestServerName` | `zx9s1Lk+O90` | +| `sceHttp2SetStripGZIPHeadersEnabled` | `mMWZm0+7XHM` | +| `sceHttp2WebSocketClose` | `v2hFBiyS13w` | +| `sceHttp2WebSocketSendDataMessage` | `D1IAo-CNswA` | +| `sceHttp2WebSocketSendTextMessage` | `jSHMX1oSE+E` | +| `sceHttp2WebSocketSetCloseTimeout` | `4PUc7B6Zr+c` | + - [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 - [libSceHttp2 cookie boxes](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` takes a machine-sized pool size and requires at least 64 KiB. On 13.42 the pool is rounded up to 16 KiB (wrapping to 0 for `SIZE_MAX`) and mapped with `sceKernelMapNamedFlexibleMemory`, whose error is returned unchanged; the host maps it the same way under an assumed name, but does not reproduce native cookie-heap bookkeeping, identifier ranges or retained request references. Its delete signature and invalid-ID error are confirmed. `sceHttp2SetRequestNoContentLength` takes only the request id. `sceHttp2CookieFlush` empties a cookie box and answers 0 for any other id. Nonzero cookie-box binding by `sceHttp2SetCookieBox` throws - [libSceHttp2 cookie store](../../../core/libs/prx/libSceHttp2/Cookies.cpp) (libSceHttp2) - behavior follows a run of `GuestHttp2Cookies.cpp`'s sequence against 13.42's `libSceHttp2.sprx` ([#10534](https://github.com/boykopovar/AnyPS5/pull/10534)). Not reproduced: the `0x817B1076` stack check below 0x1FD0 bytes of stack, and the pool size is not enforced. Assumed, not measured on libSceHttp2: the default total limit of 3000 with the 10 oldest dropped past it, and the NUL, space, NUL written after the header (both libSceHttp's), which of no match and null `cookie`/`required` is checked first, public-suffix rules for `Domain`, and dates beyond the tested ones. Equal-length paths come out with the first stored last, a rule inferred from the runs From fadf15081d7280d0ba7a562e47e1988ca85d874c Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Tue, 27 Oct 2026 12:28:30 +0000 Subject: [PATCH 509/702] feat(libSceAudioOut): declare 11 additional exports --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 1 + .../include/InventoryExports.hpp | 19 +++++++ .../prx/libSceAudioOut/libSceAudioOut.hpp | 1 + .../libSceAudioOut/src/InventoryExports.cpp | 50 +++++++++++++++++++ docs/dev/debt/libSceAudioOut.md | 16 ++++++ 5 files changed, 87 insertions(+) create mode 100644 core/libs/prx/libSceAudioOut/include/InventoryExports.hpp create mode 100644 core/libs/prx/libSceAudioOut/src/InventoryExports.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 7c97d07a55..1caf8fb405 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2User.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Mastering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2System.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/InventoryExports.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Unimplemented.cpp ) diff --git a/core/libs/prx/libSceAudioOut/include/InventoryExports.hpp b/core/libs/prx/libSceAudioOut/include/InventoryExports.hpp new file mode 100644 index 0000000000..13bd1029e9 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/include/InventoryExports.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceAudioOut2GetSystemDebugState(); +int APS5_VABI sceAudioOut2LoContextCreateEx(); +int APS5_VABI sceAudioOut2LoContextGetAttributes(); +int APS5_VABI sceAudioOut2LoContextQueryMemoryEx(); +int APS5_VABI sceAudioOut2LoPortCreateEx(); +int APS5_VABI sceAudioOut2LoPortGetAttributes(); +int APS5_VABI sceAudioOut2LoPortSetAttributesEx(); +int APS5_VABI sceAudioOut2PortSetAttributesEx(); +int APS5_VABI sceAudioOut2SetHrtfIdForCronos(); +int APS5_VABI sceAudioOut2SetSpeakerInfo(); +int APS5_VABI sceAudioOut2SetTvCorrectionInfo(); + +} diff --git a/core/libs/prx/libSceAudioOut/libSceAudioOut.hpp b/core/libs/prx/libSceAudioOut/libSceAudioOut.hpp index d092ed95af..da1886b340 100644 --- a/core/libs/prx/libSceAudioOut/libSceAudioOut.hpp +++ b/core/libs/prx/libSceAudioOut/libSceAudioOut.hpp @@ -2,3 +2,4 @@ #include "prx/libSceAudioOut/include/AudioOut.hpp" #include "prx/libSceAudioOut/include/AudioOut2.hpp" +#include "prx/libSceAudioOut/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceAudioOut/src/InventoryExports.cpp b/core/libs/prx/libSceAudioOut/src/InventoryExports.cpp new file mode 100644 index 0000000000..f6c3567919 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/InventoryExports.cpp @@ -0,0 +1,50 @@ +#include "prx/libSceAudioOut/include/InventoryExports.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudioOut2GetSystemDebugState() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2LoContextCreateEx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2LoContextGetAttributes() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2LoContextQueryMemoryEx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2LoPortCreateEx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2LoPortGetAttributes() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2LoPortSetAttributesEx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2PortSetAttributesEx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2SetHrtfIdForCronos() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2SetSpeakerInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOut2SetTvCorrectionInfo() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 7946f949d4..e3e8cb0aca 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -8,6 +8,22 @@ ### Unknown function info +- [libSceAudioOut additional exports](../../../core/libs/prx/libSceAudioOut/src/InventoryExports.cpp) - the following name/NID pairs are present in the 13.60 export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +| Name | NID | +| --- | --- | +| `sceAudioOut2GetSystemDebugState` | `1kUphng3rOk` | +| `sceAudioOut2LoContextCreateEx` | `o+Ol1OcMbBo` | +| `sceAudioOut2LoContextGetAttributes` | `Nb3DK8hNqeY` | +| `sceAudioOut2LoContextQueryMemoryEx` | `+8fuZ1rh4PA` | +| `sceAudioOut2LoPortCreateEx` | `eD21Kabealg` | +| `sceAudioOut2LoPortGetAttributes` | `45qxRgFYZZg` | +| `sceAudioOut2LoPortSetAttributesEx` | `3o6lDIe0iY0` | +| `sceAudioOut2PortSetAttributesEx` | `11yhLGUG49M` | +| `sceAudioOut2SetHrtfIdForCronos` | `PA3AjhcS+w4` | +| `sceAudioOut2SetSpeakerInfo` | `-Bn3G37Q1qU` | +| `sceAudioOut2SetTvCorrectionInfo` | `slskcaPR5u0` | + - [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. Completed grains remain queued until Push consumes them, so memory use grows with an unconsumed PCM backlog. The console backlog limit is unverified. A nonblocking Push without a completed grain throws; this is a runtime restriction, not a console measurement. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. - [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) - the PS5 module confirms a pointer-sized user handle and two optional uint32 outputs. Handles 0/-1 return 0x80268010 before checking outputs; two null outputs return 0x80268001. Host masks still describe only implemented PCM/gain support, unlike native SDK- and device-dependent masks. Unknown or destroyed host handles throw; native arbitrary handles are pointers and cannot be validated by the host registry. Port ownership and busy-user destruction remain unimplemented. From 1c2667ff635dcb401f22f35c109a09cd9ba96010 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 12:32:51 +0000 Subject: [PATCH 510/702] docs(libSceSystemService): list the splash, player dialog and notice-flag calls as silent stubs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceSystemService.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index c93bb89e5a..8d040b49c7 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -12,6 +12,8 @@ - [sceSystemServiceShowDisplaySafeAreaSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the display safe area settings dialog: AnyPS5 has no system UI to open. PPSA03332 imports it, and the no-argument signature is assumed: [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) has `sceSystemServiceShowControllerSettings` but not this call - [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it - [sceSystemServiceOpenTournamentOccurrence](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the tournament occurrence: AnyPS5 has no system UI to open +- [sceSystemServiceHideSplashScreen, sceSystemServiceLaunchPlayerDialog](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - return success and do nothing: AnyPS5 shows no splash screen to hide and has no system UI to open the player dialog in. All ten titles checked call `sceSystemServiceHideSplashScreen`. Borderlands 4 (PPSA01494), Ghost of Tsushima (PPSA02225), Metal Gear Solid 4 (PPSA18194), MXGP 26 (PPSA30839), Super Meat Boy 3D (PPSA31162), Call of Duty: Black Ops II (PPSA34502) and Steins;Gate Re:Boot (PPSA36591) call `sceSystemServiceLaunchPlayerDialog` +- [sceSystemServiceSetNoticeScreenSkipFlag, sceSystemServiceDisableNoticeScreenSkipFlagAutoSet](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - return success without storing anything, and `sceSystemServiceGetNoticeScreenSkipFlag` always reports `false`. Metal Gear Solid 4 (PPSA18194) imports both and calls `sceSystemServiceDisableNoticeScreenSkipFlagAutoSet` once ### Unknown function info From 0f75fd06629a9f113de783414d861b0fff1553e8 Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:38:18 +0000 Subject: [PATCH 511/702] fix(relinker): accept an identical duplicate pointer relocation in the strict filter A RELA entry repeated with the same target, type and addend writes the same value twice, so applying it once is equivalent. 9673868d5f made the Windows relocation builder and the code analysis accept such duplicates, but the strict unused-NID filter still refused one as overlapping, so the same ELF converted with unused-filter=0 or 1 and failed with unused-filter=2. The strict filter now records a pointer relocation only when its slot is new or its target is identical, and still refuses a distinct entry at one slot and a slot that an import relocation already owns. AI-assisted: yes --- .../UnusedNidFilter/StrictNidFilter.cpp | 3 +- .../relinker/tests/StrictNidFilterTests.cpp | 45 +++++++++++++++++++ .../tests/test_duplicate_relocations.py | 42 +++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index 75d0da66f9..b63d3c329c 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -85,7 +85,8 @@ public: throw RelinkerException("Strict filter: relative relocation has a symbol", slot); } const auto target = read<std::uint64_t>(*rela + offset + 16); - if (input.ImportSlots.contains(slot) || !input.Pointers.emplace(slot, target).second) + const auto [recorded, inserted] = input.Pointers.emplace(slot, target); + if (input.ImportSlots.contains(slot) || (!inserted && recorded->second != target)) throw RelinkerException("Strict filter: overlapping pointer relocations", slot); } diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index 349da2d7a0..76a811be45 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -672,6 +672,50 @@ void sceDynlibDataOffsets() { "Strict filter accepted a RELATIVE relocation symbol outside the symbol table"); } +void duplicatePointerRelocations() { + auto input = fixture(); + emit(input, 0, {0xC3}); + importThunk(input, 16, 0x2000); + auto bytes = elfFixture(input); + bytes.resize(0xA00); + write<std::uint16_t>(bytes, 56, 4); + write<std::uint32_t>(bytes, 176, 2); + write<std::uint32_t>(bytes, 180, 6); + write<std::uint64_t>(bytes, 184, 0x300); + write<std::uint64_t>(bytes, 192, 0x2000); + write<std::uint64_t>(bytes, 208, 0x40); + write<std::uint64_t>(bytes, 216, 0x40); + write<std::uint64_t>(bytes, 224, 8); + write<std::uint32_t>(bytes, 232, 0x61000000); + write<std::uint32_t>(bytes, 236, 4); + write<std::uint64_t>(bytes, 240, 0x500); + write<std::uint64_t>(bytes, 264, 0x500); + write<std::uint64_t>(bytes, 272, 0x500); + write<std::uint64_t>(bytes, 280, 0x10); + write<std::int64_t>(bytes, 0x300, 0x6100002F); + write<std::uint64_t>(bytes, 0x308, 0x400); + write<std::int64_t>(bytes, 0x310, 0x61000031); + write<std::uint64_t>(bytes, 0x318, 48); + write<std::int64_t>(bytes, 0x320, 0x61000033); + write<std::uint64_t>(bytes, 0x328, 24); + write<std::uint64_t>(bytes, 0x900, 0x2040); + write<std::uint64_t>(bytes, 0x908, 8); + write<std::uint64_t>(bytes, 0x910, 0x1010); + write<std::uint64_t>(bytes, 0x918, 0x2040); + write<std::uint64_t>(bytes, 0x920, 8); + write<std::uint64_t>(bytes, 0x928, 0x1010); + const std::vector<Relinker::NidReference> references = {{"live", {}, 6, 0x900, 0x2000, 0}}; + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr).size() == 1, + "Strict filter rejected an identical duplicate pointer relocation"); + write<std::uint64_t>(bytes, 0x928, 0x1020); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr); }, + "Strict filter accepted a conflicting duplicate pointer relocation"); + write<std::uint64_t>(bytes, 0x928, 0x1010); + write<std::uint64_t>(bytes, 0x918, 0x2000); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr); }, + "Strict filter accepted a pointer relocation overlapping an import slot"); +} + } int main() { @@ -696,6 +740,7 @@ int main() { filterAndPltCompaction(); relativeRelocationTableBounds(); sceDynlibDataOffsets(); + duplicatePointerRelocations(); std::cout << "Strict NID filter tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/relinker/tests/test_duplicate_relocations.py b/core/relinker/relinker/tests/test_duplicate_relocations.py index b0920bfe16..e06aef8876 100644 --- a/core/relinker/relinker/tests/test_duplicate_relocations.py +++ b/core/relinker/relinker/tests/test_duplicate_relocations.py @@ -5,6 +5,7 @@ import tempfile from pathlib import Path from test_guest_none_relocations import module +from test_guest_symbol_names import DYNAMIC, RELOCATIONS, importing_executable from test_overlapping_relocations import TARGETS, check_output, fixture @@ -19,6 +20,38 @@ def with_second_entry(entry): raise AssertionError("no DT_RELASZ tag") +def strict_fixture(second_addend=None): + image = importing_executable(["live"]) + dynamic_size, = struct.unpack_from("<Q", image, 152) + struct.pack_into("<qQ", image, DYNAMIC + dynamic_size - 16, 0x6100003F, 48) + struct.pack_into("<qQ", image, DYNAMIC + dynamic_size, 0, 0) + struct.pack_into("<QQ", image, 152, dynamic_size + 16, dynamic_size + 16) + image[0x4020:0x4026] = b"\xb8\x11\x00\x00\x00\xc3" + struct.pack_into("<QQq", image, RELOCATIONS + 24, 0x4E08, 8, 0x4020) + if second_addend is not None: + struct.pack_into("<QQq", image, RELOCATIONS + 48, 0x4E08, 8, second_addend) + for offset in range(DYNAMIC, DYNAMIC + dynamic_size, 16): + tag, value = struct.unpack_from("<qQ", image, offset) + if tag == 8: + struct.pack_into("<qQ", image, offset, tag, 48 if second_addend is None else 72) + return image + raise AssertionError("no DT_RELASZ tag") + + +def strict_convert(relinker, work, name, image): + source = work / f"strict-{name}.elf" + source.write_bytes(image) + output = work / f"strict-{name}.out" + result = subprocess.run([str(relinker), "--skip-sce-module", "unused-filter=2", str(source), str(output)], + capture_output=True, text=True, timeout=10) + return result, output + + +def strict_reports(result): + return (next(line for line in result.stdout.splitlines() if "Strict filtering total:" in line), + next(line for line in result.stdout.splitlines() if line.startswith("Function graph:"))) + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-relocation-duplicate-") as directory: @@ -48,6 +81,15 @@ def main(): result, output = convert("conflicting", "windows", ["--windows"], conflicting) assert result.returncode == 2 and "Overlapping ELF relocation targets" in result.stderr and not output.exists(), ( result.stdout, result.stderr) + result, output = strict_convert(relinker, work, "single", strict_fixture()) + assert result.returncode == 0 and output.exists(), (result.stdout, result.stderr) + expected = strict_reports(result) + result, output = strict_convert(relinker, work, "duplicate", strict_fixture(0x4020)) + assert result.returncode == 0 and output.exists(), (result.stdout, result.stderr) + assert strict_reports(result) == expected, (result.stdout, expected) + result, output = strict_convert(relinker, work, "conflicting", strict_fixture(0x4030)) + assert result.returncode == 2 and "Strict filter: overlapping pointer relocations" in result.stderr and not output.exists(), ( + result.stdout, result.stderr) print("Duplicate relocation tests passed") From 3ab97351f22008c75d12c79d2086464ab8eeba5c Mon Sep 17 00:00:00 2001 From: Matheus Herberts <163153928+Mherberts@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:39:50 -0300 Subject: [PATCH 512/702] fix(relinker): validate visibility of undefined strong symbols --- core/relinker/CMakeLists.txt | 5 + .../relinker/src/guest/GuestImageReader.cpp | 3 + .../src/pipeline/RelinkerPipeline.cpp | 4 + .../test_restricted_undefined_symbols.py | 97 +++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 core/relinker/relinker/tests/test_restricted_undefined_symbols.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2267be074f..da708501a9 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -823,6 +823,11 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $<TARGET_FILE:relinker>) add_test(NAME null_symbol_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_null_symbol_relocations.py $<TARGET_FILE:relinker>) + foreach(symbolInput IN ITEMS executable guest) + add_test(NAME restricted_undefined_symbols_${symbolInput} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_restricted_undefined_symbols.py $<TARGET_FILE:relinker> ${symbolInput}) + set_tests_properties(restricted_undefined_symbols_${symbolInput} PROPERTIES TIMEOUT 20) + endforeach() add_test(NAME input_nonascii COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_nonascii.py $<TARGET_FILE:relinker>) add_test(NAME long_paths COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 9e058f2b6e..008fe86df0 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -241,6 +241,9 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto relocation = static_cast<std::uint32_t>(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& referencedSymbol = image.Symbols[info >> 32]; + if (relocation != 8 && referencedSymbol.Section == 0 && (referencedSymbol.Info >> 4) != 2 && (referencedSymbol.Visibility & 3) != 0) + fail("Undefined non-weak symbol has non-default visibility: " + referencedSymbol.Name); const auto target = Io::ReadU64(bytes, offset + index); const auto addend = Io::ReadU64(bytes, offset + index + 16); mapped(target, 8, 2); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index ceda75412d..4ce8ee5eae 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -396,6 +396,10 @@ RelinkResult RelinkerPipeline::Relink() { std::memcpy(&nameOff, raw.data() + symOff, 4); const auto name = readCStr(nameOff); + std::uint16_t section = 0; + std::memcpy(§ion, raw.data() + symOff + 6, sizeof(section)); + if (section == SHN_UNDEF && (raw[symOff + 4] >> 4) != STB_WEAK && (raw[symOff + 5] & 3) != 0) + throw RelinkerException("Undefined non-weak symbol has non-default visibility: " + name, symOff); if (name.empty() && (raw.size() - symOff < symEntSize || std::any_of(raw.begin() + static_cast<std::ptrdiff_t>(symOff), raw.begin() + static_cast<std::ptrdiff_t>(symOff + symEntSize), [](const std::uint8_t byte) { return byte != 0; }))) diff --git a/core/relinker/relinker/tests/test_restricted_undefined_symbols.py b/core/relinker/relinker/tests/test_restricted_undefined_symbols.py new file mode 100644 index 0000000000..0681e59993 --- /dev/null +++ b/core/relinker/relinker/tests/test_restricted_undefined_symbols.py @@ -0,0 +1,97 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_call_registry import make_image +from test_guest_intel_trampolines import main_fixture +from test_guest_none_relocations import TARGET, module + + +TARGETS = (("linux", []), ("pie", ["--lazy-binding"]), ("windows", ["--windows"]), ("macos", ["--macos"])) +ERROR = "Undefined non-weak symbol has non-default visibility: " + + +def executable(relocation, visibility, binding=1, referenced=True): + image = make_image(b"\0lib.so\0symbol\0") + image[0x63c] = (binding << 4) | 2 + image[0x63d] = visibility + struct.pack_into("<QQq", image, 0x700, 0x300, (1 << 32) | (relocation if referenced else 0), 0) + if relocation == 7: + tags = [(1, 1), (5, 0x600), (10, 15), (6, 0x620), (11, 24), (0x6100003f, 48), + (3, 0x300), (23, 0x700), (2, 24 if referenced else 0), (20, 7), (0, 0)] + for index, tag in enumerate(tags): + struct.pack_into("<qQ", image, 0x400 + index * 16, *tag) + struct.pack_into("<QQ", image, 120 + 32, len(tags) * 16, len(tags) * 16) + return image + + +def guest(relocation, visibility, binding=1, referenced=True, defined=False, sce=False): + entry = (TARGET, (1 << 32) | (relocation if referenced else 0), 0) + image = module(plt=[entry] if relocation == 7 else (), + rela=[entry] if relocation != 7 else (), exported=defined, sce=sce) + image[0x229c] = (binding << 4) | 2 + image[0x229d] = visibility + return image + + +def main(): + relinker = Path(sys.argv[1]).resolve() + is_guest = sys.argv[2] == "guest" + count = 0 + with tempfile.TemporaryDirectory(prefix="anyps5-restricted-symbols-") as directory: + work = Path(directory) + + def convert(name, image, options, rejected): + nonlocal count + case = work / f"case-{count}" + case.mkdir() + source = case / "input.elf" + source.write_bytes(main_fixture() if is_guest else image) + if is_guest: + modules = case / "sce_module" + modules.mkdir() + module_path = modules / "restricted.prx" + module_path.write_bytes(image) + output = case / "output.out" + command = [str(relinker), *options, str(source), str(output)] + if not is_guest: + command.insert(1, "--skip-sce-module") + result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", timeout=20) + if rejected: + symbol = "shared" if is_guest else "symbol" + assert result.returncode == 2 and ERROR + symbol in result.stderr, ( + name, options, result.returncode, result.stdout, result.stderr) + assert not output.exists() and not (case / "app0").exists(), (name, options) + else: + assert result.returncode == 0 and output.is_file(), ( + name, options, result.returncode, result.stdout, result.stderr) + assert source.read_bytes() == (main_fixture() if is_guest else image), (name, options) + if is_guest: + assert module_path.read_bytes() == image, (name, options) + count += 1 + + for relocation in (1, 6, 7): + for target, options in TARGETS: + if is_guest and target == "pie": + continue + for sce in ((False, True) if is_guest else (False,)): + make = (lambda visibility, **kw: guest(relocation, visibility, sce=sce, **kw)) if is_guest else ( + lambda visibility, **kw: executable(relocation, visibility, **kw)) + label = f"{relocation}-{target}-{sce}" + for visibility in (1, 2, 3): + convert(label + f"-visibility-{visibility}", make(visibility), options, True) + convert(label + "-local-hidden", make(2, binding=0), options, True) + convert(label + "-default", make(0), options, False) + convert(label + "-weak-default", make(0, binding=2), options, False) + convert(label + "-unreferenced", make(2, referenced=False), options, False) + if is_guest: + convert(label + "-defined-hidden", make(2, defined=True), options, False) + else: + convert(label + "-filtered", make(2), [*options, "unused-filter=1"], True) + print(f"Restricted undefined symbol tests passed: {count} conversions ({'guest' if is_guest else 'executable'})") + + +if __name__ == "__main__": + main() From 7fa95507998e874ff0024164dcc441e15264d7af Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 12:41:26 +0000 Subject: [PATCH 513/702] docs(libSceNpManager): list the premium-feature, title ID and premium callback calls as silent stubs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceNpManager.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 8afcb90846..57847beb5d 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -3,6 +3,9 @@ ### Silent stubs - [sceNpUnregisterPlusEventCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - returns 0 whether or not a callback was registered, as `sceNpUnregisterPremiumEventCallback` does: `sceNpRegisterPlusEventCallback` keeps no callback, and a signed-out user gets no plus events +- [sceNpNotifyPremiumFeature](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - returns 0 without reading its parameter block. Borderlands 4 (PPSA01494), Ghost of Tsushima (PPSA02225), MXGP 26 (PPSA30839) and Super Meat Boy 3D (PPSA31162) call it, and Metal Gear Solid 4 (PPSA18194) imports it +- [sceNpSetNpTitleId](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - returns 0 and keeps neither its title ID nor the 128-byte block passed with it. Forspoken (PPSA03984) and Crimson Desert (PPSA25286) call it once each +- [sceNpRegisterPremiumEventCallback, sceNpUnregisterPremiumEventCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - registration checks that the callback is non-null but keeps neither it nor its user data, so it is never called, and unregistration returns 0. Metal Gear Solid 4 (PPSA18194) imports both ### Unknown function info From 498270408fb6a59b8af3b0cb5539ee3125a7d297 Mon Sep 17 00:00:00 2001 From: al3ss4ndrolb <173555546+al3ss4ndrolb@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:10:15 +0100 Subject: [PATCH 514/702] fix(libkernel): build the param.json parser with Clang and libstdc++ JsonObject held std::pair<std::string, JsonValue> while JsonValue was still incomplete. Clang with libstdc++ instantiates the pair and the vector destructor at the end of JsonValue and stops with "incomplete type used in type trait expression". A named JsonMember struct defined after JsonValue keeps the vector element a plain class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../AppMetadata/src/ParamJsonParser.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 3150e1419a..fd635261f2 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -16,9 +16,10 @@ namespace { enum class JsonType { Null, Boolean, Number, String, Array, Object }; struct JsonValue; +struct JsonMember; using JsonArray = std::vector<JsonValue>; -using JsonObject = std::vector<std::pair<std::string, JsonValue>>; +using JsonObject = std::vector<JsonMember>; struct JsonValue { JsonType type = JsonType::Null; @@ -30,6 +31,11 @@ struct JsonValue { JsonObject objectValue; }; +struct JsonMember { + std::string key; + JsonValue value; +}; + std::string readFileToString(const std::filesystem::path& path) { std::ifstream file(path, std::ios::binary); if (!file.is_open()) throw std::runtime_error("failed to open param.json"); @@ -179,7 +185,7 @@ JsonValue parseJsonObject(const std::string& text, std::size_t& position, std::s skipWhitespace(text, position); expectChar(text, position, ':'); JsonValue member = parseJsonValue(text, position, depth + 1); - value.objectValue.emplace_back(std::move(key), std::move(member)); + value.objectValue.push_back({std::move(key), std::move(member)}); skipWhitespace(text, position); const char next = peekChar(text, position); if (next == ',') { ++position; continue; } @@ -275,7 +281,7 @@ JsonValue parseJsonDocument(const std::string& text) { const JsonValue* findObjectMember(const JsonValue& object, const std::string& key) { if (object.type != JsonType::Object) throw std::runtime_error("expected JSON object in param.json"); for (const auto& member : object.objectValue) { - if (member.first == key) return &member.second; + if (member.key == key) return &member.value; } return nullptr; } @@ -308,8 +314,8 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { if (languageObject == nullptr) languageObject = findObjectMember(*localizedParametersValue, "en-US"); if (languageObject == nullptr) { for (const auto& member : localizedParametersValue->objectValue) { - if (member.second.type == JsonType::Object) { - languageObject = &member.second; + if (member.value.type == JsonType::Object) { + languageObject = &member.value; break; } } From 8fe03aa952219b929ffec332a084c925c2fe6f7e Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 12:47:09 +0000 Subject: [PATCH 515/702] docs(libSceSystemService): record the null-parameter checks in the splash and notice-flag entries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceSystemService.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 8d040b49c7..f43060475c 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -12,8 +12,8 @@ - [sceSystemServiceShowDisplaySafeAreaSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the display safe area settings dialog: AnyPS5 has no system UI to open. PPSA03332 imports it, and the no-argument signature is assumed: [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) has `sceSystemServiceShowControllerSettings` but not this call - [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it - [sceSystemServiceOpenTournamentOccurrence](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the tournament occurrence: AnyPS5 has no system UI to open -- [sceSystemServiceHideSplashScreen, sceSystemServiceLaunchPlayerDialog](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - return success and do nothing: AnyPS5 shows no splash screen to hide and has no system UI to open the player dialog in. All ten titles checked call `sceSystemServiceHideSplashScreen`. Borderlands 4 (PPSA01494), Ghost of Tsushima (PPSA02225), Metal Gear Solid 4 (PPSA18194), MXGP 26 (PPSA30839), Super Meat Boy 3D (PPSA31162), Call of Duty: Black Ops II (PPSA34502) and Steins;Gate Re:Boot (PPSA36591) call `sceSystemServiceLaunchPlayerDialog` -- [sceSystemServiceSetNoticeScreenSkipFlag, sceSystemServiceDisableNoticeScreenSkipFlagAutoSet](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - return success without storing anything, and `sceSystemServiceGetNoticeScreenSkipFlag` always reports `false`. Metal Gear Solid 4 (PPSA18194) imports both and calls `sceSystemServiceDisableNoticeScreenSkipFlagAutoSet` once +- [sceSystemServiceHideSplashScreen, sceSystemServiceLaunchPlayerDialog](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 shows no splash screen to hide and has no system UI to open the player dialog in. `sceSystemServiceHideSplashScreen` returns success, and `sceSystemServiceLaunchPlayerDialog` returns `SYSTEM_SERVICE_ERROR_PARAMETER` for a null parameter and success otherwise, without reading the parameter. All ten titles checked call `sceSystemServiceHideSplashScreen`. Borderlands 4 (PPSA01494), Ghost of Tsushima (PPSA02225), Metal Gear Solid 4 (PPSA18194), MXGP 26 (PPSA30839), Super Meat Boy 3D (PPSA31162), Call of Duty: Black Ops II (PPSA34502) and Steins;Gate Re:Boot (PPSA36591) call `sceSystemServiceLaunchPlayerDialog` +- [sceSystemServiceSetNoticeScreenSkipFlag, sceSystemServiceDisableNoticeScreenSkipFlagAutoSet](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - return success without storing anything. `sceSystemServiceGetNoticeScreenSkipFlag` returns `SYSTEM_SERVICE_ERROR_PARAMETER` for a null output and otherwise always reports `false`. Metal Gear Solid 4 (PPSA18194) imports both and calls `sceSystemServiceDisableNoticeScreenSkipFlagAutoSet` once ### Unknown function info From af244a774830b5914a5ad7f77558e1bebf1579c4 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 12:47:26 +0000 Subject: [PATCH 516/702] docs(libSceNpManager): record the null-callback check in the premium callback entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceNpManager.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 57847beb5d..78650ea0a3 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -5,7 +5,7 @@ - [sceNpUnregisterPlusEventCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - returns 0 whether or not a callback was registered, as `sceNpUnregisterPremiumEventCallback` does: `sceNpRegisterPlusEventCallback` keeps no callback, and a signed-out user gets no plus events - [sceNpNotifyPremiumFeature](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - returns 0 without reading its parameter block. Borderlands 4 (PPSA01494), Ghost of Tsushima (PPSA02225), MXGP 26 (PPSA30839) and Super Meat Boy 3D (PPSA31162) call it, and Metal Gear Solid 4 (PPSA18194) imports it - [sceNpSetNpTitleId](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - returns 0 and keeps neither its title ID nor the 128-byte block passed with it. Forspoken (PPSA03984) and Crimson Desert (PPSA25286) call it once each -- [sceNpRegisterPremiumEventCallback, sceNpUnregisterPremiumEventCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - registration checks that the callback is non-null but keeps neither it nor its user data, so it is never called, and unregistration returns 0. Metal Gear Solid 4 (PPSA18194) imports both +- [sceNpRegisterPremiumEventCallback, sceNpUnregisterPremiumEventCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - registration returns `SCE_NP_ERROR_INVALID_ARGUMENT` for a null callback and otherwise returns 0 without keeping the callback or its user data, so it is never called. Unregistration returns 0. Metal Gear Solid 4 (PPSA18194) imports both ### Unknown function info From 91624a508eeab3271659e4466a64ca483817d845 Mon Sep 17 00:00:00 2001 From: Giannis <johngianno2000@gmail.com> Date: Tue, 27 Oct 2026 14:33:17 +0200 Subject: [PATCH 517/702] test(agc): skip the 4_4_4_4 partial write test without shaderStorageImageReadWithoutFormat agc_driver_packed_unorm4444_partial_write failed on devices without shaderStorageImageReadWithoutFormat (Intel HD 630 on Mesa ANV): a partial write merges into the target in the pixel shader, and EmitStorageImageRead refuses the read of the target ("storage image reads without a format are unavailable on the target device"). The test checked only fragmentShaderPixelInterlock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../tests/execution/PackedUnorm4444PartialWrite.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 1acad9581f..4afab26121 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -475,6 +475,10 @@ int main() { std::puts("skipped, the device lacks fragmentShaderPixelInterlock"); return VulkanTestSkipped; } + if (!TargetHasCapability(device->Target(), spv::CapabilityStorageImageReadWithoutFormat)) { + std::puts("skipped, the device lacks shaderStorageImageReadWithoutFormat"); + return VulkanTestSkipped; + } const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 2); Block block(layout.Bytes()); const auto color = DecodeTarget(block); From 819cf2eb0a69a41341e934adc553bf809a7872b7 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 12:56:23 +0000 Subject: [PATCH 518/702] docs(libSceAmpr): list the command buffer destructors and the AMM constructor as silent stubs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAmpr.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index 83ecbcb38d..31e3d8fd45 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -1,5 +1,9 @@ # libSceAmpr technical debt +### Silent stubs + +- [sceAmprCommandBufferDestructor, sceAmprAprCommandBufferDestructor, sceAmprAmmCommandBufferConstructor, sceAmprAmmCommandBufferDestructor](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - return 0 without reading or changing the buffer, or the APR gather and scatter states. Unlike `sceAmprCommandBufferConstructor` and `sceAmprAprCommandBufferConstructor`, the AMM constructor leaves every field of the buffer as the caller left it. Borderlands 4 (PPSA01494), Ghost of Tsushima (PPSA02225), Forspoken (PPSA03984), Astro Bot (PPSA21564), Crimson Desert (PPSA25286), MXGP 26 (PPSA30839) and Super Meat Boy 3D (PPSA31162) call both destructors; Forspoken also calls the AMM constructor and destructor once each + ### Unknown function info - [AMPR command buffer null arguments](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; these return `SCE_KERNEL_ERROR_EINVAL` here, but the behavior has not been confirmed on a PS5. From 094868fab249426edc65051c75cea78cb64e6ae0 Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Tue, 27 Oct 2026 12:57:10 +0000 Subject: [PATCH 519/702] feat(libSceCamera): declare 4 additional exports --- core/libs/prx/libSceCamera/CMakeLists.txt | 1 + .../prx/libSceCamera/InventoryExports.cpp | 22 +++++++++++++++++++ .../libSceCamera/include/InventoryExports.hpp | 12 ++++++++++ core/libs/prx/libSceCamera/libSceCamera.hpp | 1 + docs/dev/debt/libSceCamera.md | 11 ++++++++++ 5 files changed, 47 insertions(+) create mode 100644 core/libs/prx/libSceCamera/InventoryExports.cpp create mode 100644 core/libs/prx/libSceCamera/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceCamera/CMakeLists.txt b/core/libs/prx/libSceCamera/CMakeLists.txt index 8591dce44f..f7c5e9f985 100644 --- a/core/libs/prx/libSceCamera/CMakeLists.txt +++ b/core/libs/prx/libSceCamera/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(libSceCamera SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/InventoryExports.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Handles.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Attributes.cpp diff --git a/core/libs/prx/libSceCamera/InventoryExports.cpp b/core/libs/prx/libSceCamera/InventoryExports.cpp new file mode 100644 index 0000000000..5251fcf228 --- /dev/null +++ b/core/libs/prx/libSceCamera/InventoryExports.cpp @@ -0,0 +1,22 @@ +#include "prx/libSceCamera/include/InventoryExports.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCameraGetUsageInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCameraGetUsageInfoWithoutCountProcess() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCameraOpenByModuleIdWithMode() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCameraSetForceDriverMode() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceCamera/include/InventoryExports.hpp b/core/libs/prx/libSceCamera/include/InventoryExports.hpp new file mode 100644 index 0000000000..7b4ea82928 --- /dev/null +++ b/core/libs/prx/libSceCamera/include/InventoryExports.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceCameraGetUsageInfo(); +int APS5_VABI sceCameraGetUsageInfoWithoutCountProcess(); +int APS5_VABI sceCameraOpenByModuleIdWithMode(); +int APS5_VABI sceCameraSetForceDriverMode(); + +} diff --git a/core/libs/prx/libSceCamera/libSceCamera.hpp b/core/libs/prx/libSceCamera/libSceCamera.hpp index f11c2f4973..c457e42108 100644 --- a/core/libs/prx/libSceCamera/libSceCamera.hpp +++ b/core/libs/prx/libSceCamera/libSceCamera.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceCamera/include/Api.hpp" +#include "prx/libSceCamera/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceCamera.md b/docs/dev/debt/libSceCamera.md index cf3e053ff2..721388d3c1 100644 --- a/docs/dev/debt/libSceCamera.md +++ b/docs/dev/debt/libSceCamera.md @@ -1,5 +1,16 @@ # libSceCamera technical debt +### Unknown function info + +- [libSceCamera additional exports](../../../core/libs/prx/libSceCamera/InventoryExports.cpp) - these name/NID pairs are present in the 13.60 export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +| Name | NID | +| --- | --- | +| `sceCameraGetUsageInfo` | `BTUze28HY6k` | +| `sceCameraGetUsageInfoWithoutCountProcess` | `9dHXTyfIUec` | +| `sceCameraOpenByModuleIdWithMode` | `inXzc8jI6+Y` | +| `sceCameraSetForceDriverMode` | `IBBi2SqEtoY` | + ### Functional - [sceCameraIsAttached](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera) - no camera is ever attached: index 0 returns 0 (not attached) and any other index returns `SCE_CAMERA_ERROR_PARAM` (0x802E0000), as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera.cpp#L187-L200) does without a configured camera, with the error value from its [camera_error.h](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera_error.h). Not measured on a console. Closes when: a host camera is exposed to guests. From 8d08a1338ab0c6affdf06a352e5fe5f824bf3650 Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Tue, 27 Oct 2026 12:57:10 +0000 Subject: [PATCH 520/702] feat(libSceVideodec2): declare 3 additional exports --- core/libs/prx/libSceVideodec2/CMakeLists.txt | 1 + .../prx/libSceVideodec2/InventoryExports.cpp | 18 ++++++++++++++++++ .../include/InventoryExports.hpp | 11 +++++++++++ .../prx/libSceVideodec2/libSceVideodec2.hpp | 1 + docs/dev/debt/libSceVideodec2.md | 8 ++++++++ 5 files changed, 39 insertions(+) create mode 100644 core/libs/prx/libSceVideodec2/InventoryExports.cpp create mode 100644 core/libs/prx/libSceVideodec2/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceVideodec2/CMakeLists.txt b/core/libs/prx/libSceVideodec2/CMakeLists.txt index 244b8050b3..3f47dbe0de 100644 --- a/core/libs/prx/libSceVideodec2/CMakeLists.txt +++ b/core/libs/prx/libSceVideodec2/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(libSceVideodec2 SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/InventoryExports.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HevcMetadata.cpp diff --git a/core/libs/prx/libSceVideodec2/InventoryExports.cpp b/core/libs/prx/libSceVideodec2/InventoryExports.cpp new file mode 100644 index 0000000000..714435a324 --- /dev/null +++ b/core/libs/prx/libSceVideodec2/InventoryExports.cpp @@ -0,0 +1,18 @@ +#include "prx/libSceVideodec2/include/InventoryExports.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVideodec2DecodeFirstSlice() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2DecodeNextSlice() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2SyncAllSlice() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceVideodec2/include/InventoryExports.hpp b/core/libs/prx/libSceVideodec2/include/InventoryExports.hpp new file mode 100644 index 0000000000..4e29454162 --- /dev/null +++ b/core/libs/prx/libSceVideodec2/include/InventoryExports.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceVideodec2DecodeFirstSlice(); +int APS5_VABI sceVideodec2DecodeNextSlice(); +int APS5_VABI sceVideodec2SyncAllSlice(); + +} diff --git a/core/libs/prx/libSceVideodec2/libSceVideodec2.hpp b/core/libs/prx/libSceVideodec2/libSceVideodec2.hpp index 85a17c3882..1ba7994a78 100644 --- a/core/libs/prx/libSceVideodec2/libSceVideodec2.hpp +++ b/core/libs/prx/libSceVideodec2/libSceVideodec2.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceVideodec2/include/Api.hpp" +#include "prx/libSceVideodec2/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 66e1a1ff5e..2fb2654ca2 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -2,6 +2,14 @@ ### Unknown function info +- [libSceVideodec2 additional exports](../../../core/libs/prx/libSceVideodec2/InventoryExports.cpp) - these name/NID pairs are present in the 13.60 export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +| Name | NID | +| --- | --- | +| `sceVideodec2DecodeFirstSlice` | `4auo78aivlc` | +| `sceVideodec2DecodeNextSlice` | `bGyBk6V09oM` | +| `sceVideodec2SyncAllSlice` | `JjvhoNUnZT8` | + - [sceVideodec2InitLibrary](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) throws: the FFmpeg decoders this module creates need no library-level initialisation, and neither SharpProspero nor EVO-PLAYER-PS5 declares this call, so its arguments and result are unknown. A title imports it from this module - [sceVideodec2TermLibrary and eleven other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder` and `QueryHevcDecoderMemoryInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here From 610099db74e67f1f636bac4daf50f51b528f7522 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 12:58:25 +0000 Subject: [PATCH 521/702] docs(libSceAjm.native): list the module registration, finalize and batch error dump calls as silent stubs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAjm.native.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 7124f3a574..271ae2e9c4 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -1,5 +1,11 @@ # libSceAjm.native technical debt +### Silent stubs + +- [sceAjmModuleRegister, sceAjmModuleUnregister](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - return 0 without recording the codec, so instances are created the same whether or not their codec's module was registered. Borderlands 4 (PPSA01494), Ghost of Tsushima (PPSA02225), Forspoken (PPSA03984), Metal Gear Solid 4 (PPSA18194), Crimson Desert (PPSA25286), MXGP 26 (PPSA30839) and Super Meat Boy 3D (PPSA31162) call both; Call of Duty: Black Ops II (PPSA34502) calls `sceAjmModuleRegister` once +- [sceAjmFinalize](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - returns 0 without releasing anything for the context, and the next `sceAjmInitialize` hands out a new context number. The seven titles above that unregister modules also call it +- [sceAjmBatchErrorDump](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - ignores the batch information and returns 0 with the error record zeroed when one is passed. Borderlands 4, Forspoken, Metal Gear Solid 4, MXGP 26 and Super Meat Boy 3D call it + ### Unknown function info - [sceAjmBatchGetLog, sceAjmInstanceAvailable](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From 6e58d5b0b666e5942b6a96c8b89a8cb990df9e25 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 13:02:40 +0000 Subject: [PATCH 522/702] docs(libSceVideoOut): note which open params ten titles pass to sceVideoOutOpen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceVideoOut.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index e2d0545748..68add167ae 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -3,7 +3,7 @@ ### Silent stubs - [sceVideoOutSetFlipRate](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) stores a rate of 0, 1 or 2 and returns `VIDEO_OUT_ERROR_INVALID_VALUE` for any other rate. PPSA13356 calls it with 60; whether the console accepts that value or returns this error is unverified. Its one call in PPSA13356 (0xbedef) passes the handle from a field of its own object and does not read the result. -- [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling +- [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling. Of the 16 calls in ten titles checked, all pass user 255 and index 0, 13 pass bus type 0 and three read it from a field (one call each in Borderlands 4, MXGP 26 and Super Meat Boy 3D), and 13 pass no open param. Metal Gear Solid 4 (PPSA18194) passes one from `mgs4.elf` with neither flag set and one from its launcher that sets only the affinity, 0x3e. Astro Bot (PPSA21564) sets priority 256 and affinity 0x1fff - [sceVideoOutAddVrrStatusFlagsPrivilege](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) returns 0 and does nothing: the host output has no VRR state to report. The name comes from an assert message in a title that requires `SCE_VIDEO_OUT_OK`; the NID hashes to it, the arguments are unknown (it is called without any) - [sceVideoOutVrrPegToFixedRate, sceVideoOutVrrUnpegFromFixedRate](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA05684 passes the handle to both, two further zero arguments to `sceVideoOutVrrPegToFixedRate`, and ignores the results. The meaning of those two arguments is unknown, so only zeros are accepted. VRR is not modelled and the output always runs at a fixed rate, so both calls only validate the handle and return 0 From beb46de57d5e4ba53de7035caf1a2da9a1ae3d68 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:03:02 +0100 Subject: [PATCH 523/702] fix(relinker): zero the e_ident padding of the Linux executable LinuxElfPatcher::Patch zeroed only e_ident bytes 7 and 8 of the output executable, and ElfReader checks only bytes 0 to 6, so nonzero EI_PAD bytes 9 to 15 of the input reached the output in every Linux mode. The gABI reserves EI_PAD as zero, and glibc's open_verify refuses an object with nonzero padding when ld.so maps the program itself, as under ldd or an explicit ld-linux-x86-64.so.2 invocation. The patcher now zeroes bytes 7 to 15, the rule that #11582 applies to guest modules. Only reserved bytes change, and a direct exec is unaffected either way. --- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 3 +-- .../relinker/tests/test_linux_load_alignment.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index fb493db7f0..e2a1ceb67f 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -407,8 +407,7 @@ std::vector<std::uint8_t> LinuxElfPatcher::Patch( std::vector<std::uint8_t> buf = std::move(sourceElf); const std::uint32_t type = _fixedBase ? ET_EXEC : ET_DYN; - buf[kEhdrOsAbiOffset] = 0; - buf[kEhdrAbiVersionOffset] = 0; + std::fill_n(buf.begin() + static_cast<std::ptrdiff_t>(kEhdrOsAbiOffset), 9, std::uint8_t{0}); buf[kEhdrTypeOffset] = static_cast<std::uint8_t>(type & 0xFF); buf[kEhdrTypeOffset + 1] = static_cast<std::uint8_t>((type >> 8) & 0xFF); _byteWriter->WriteU64(buf, kEhdrShOffOffset, 0); diff --git a/core/relinker/relinker/tests/test_linux_load_alignment.py b/core/relinker/relinker/tests/test_linux_load_alignment.py index 0a549b05ec..ad044624a6 100644 --- a/core/relinker/relinker/tests/test_linux_load_alignment.py +++ b/core/relinker/relinker/tests/test_linux_load_alignment.py @@ -69,6 +69,20 @@ def main(): assert warning.returncode == 0, (warning.stdout, warning.stderr) assert "--windows was not specified" in warning.stderr, warning.stderr assert named_exe.read_bytes().startswith(b"\x7fELF"), "The filename must not select the output format" + + padded = fixture() + padded[9:16] = bytes(range(1, 8)) + source.write_bytes(padded) + idents = {} + for index, mode in enumerate(([], ["--linux-fixed-base", "0x800000"], ["--lazy-binding"])): + target = Path(directory) / f"padded{index}.elf" + result = subprocess.run([str(relinker), "--skip-sce-module", *mode, str(source), str(target)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (mode, result.stdout, result.stderr) + ident = target.read_bytes()[:16] + if ident[7:] != bytes(9): + idents[" ".join(mode) or "default"] = ident.hex() + assert not idents, idents print("Linux load alignment test passed") From 17a98af8b49c47dbe3cd5470158b488e8c76357b Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 13:09:13 +0000 Subject: [PATCH 524/702] docs(libkernel): note the protections ten titles pass to sceKernelMapDirectMemory Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0fe8091311..5dc5f1cd16 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -158,7 +158,7 @@ - [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 - [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is treated as a link only when `ReadJunction` in [WindowsSymbolicLink.hpp](../../../core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp) accepts a drive-path target (`X:\...`) after stripping `\??\` / `\\?\`; volume mounts and other non-drive substitutes are not. Consumers of that drive-target path: [NativeStat.cpp](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (`lstat`/`readlink`), [Open.cpp](../../../core/libs/prx/libkernel/File/src/Open.cpp) (`O_NOFOLLOW` → `EMLINK`, and `unlink` removing the junction), [Stdio.cpp](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (`rmdir` → `ENOTDIR`), and [Export.cpp](../../../core/libs/prx/libkernel_sys/Export.cpp) (`unlinkat` with and without `AT_REMOVEDIR`). `unlink` / plain `unlinkat` leave the target, including when it is not empty - [lstat of a Windows reparse point that is not a link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a mounted volume (`\??\Volume{GUID}\`) or another name surrogate as the reparse point itself, not followed, with the mode of its attributes, as CPython's `os.lstat` does, and a reparse point without the name surrogate bit (deduplication, cloud files) as `stat` reports it -- [sceKernelMapDirectMemory with execute](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a protection with CPU execute (bit 0x4) returns `EINVAL` (0x80020016) and maps nothing, and `sceKernelMprotect` grants execute afterwards, as [PS5_PayloadSDK measured](https://github.com/mihawk-99/PS5_PayloadSDK/blob/3e31e0b1b68ada28beca68adc480cf0c3d17b9df/platform/docs/PROBE.md#L28-L34) in a title on firmware 12.09 for read-execute and read-write-execute. `sceKernelMapDirectMemory2`, `sceKernelMapNamedDirectMemory`, and `sceKernelBatchMap` and `sceKernelBatchMap2` with operation 0 share the check here, which is not measured for them, nor for a title with more privileges than a homebrew one. +- [sceKernelMapDirectMemory with execute](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a protection with CPU execute (bit 0x4) returns `EINVAL` (0x80020016) and maps nothing, and `sceKernelMprotect` grants execute afterwards, as [PS5_PayloadSDK measured](https://github.com/mihawk-99/PS5_PayloadSDK/blob/3e31e0b1b68ada28beca68adc480cf0c3d17b9df/platform/docs/PROBE.md#L28-L34) in a title on firmware 12.09 for read-execute and read-write-execute. `sceKernelMapDirectMemory2`, `sceKernelMapNamedDirectMemory`, and `sceKernelBatchMap` and `sceKernelBatchMap2` with operation 0 share the check here, which is not measured for them, nor for a title with more privileges than a homebrew one. Of 40 `sceKernelMapDirectMemory` calls in ten titles checked, 35 pass a constant protection without execute and five read it from a register or field (one each in PPSA01494, PPSA21564 and PPSA30839, two in PPSA18194). - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - 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. - [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 From 34de2385aa2482adeca8c72901f1f5be565ccede Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:10:37 +0100 Subject: [PATCH 525/702] test(tools): check the CI gate rule in one repository instead of one subprocess per case conventions_ci_gate built a git repository and ran check_conventions.py as a subprocess for each of its 35 checks, and took 40.57 s in the Windows relinker CI job 122160160306. CONTRIBUTING.md requires every test to finish in 30 s and to be shrunk when it gets close. The test now commits each case as its own workflow file in one repository, runs one real Check.workflows() in process and asserts each file's findings. Two subprocess runs keep the end-to-end path: the repository's workflows and one job without a gate. --- tools/tests/test_check_conventions_ci_gate.py | 216 ++++++++++++------ 1 file changed, 142 insertions(+), 74 deletions(-) diff --git a/tools/tests/test_check_conventions_ci_gate.py b/tools/tests/test_check_conventions_ci_gate.py index ca942cd702..9a23a88566 100644 --- a/tools/tests/test_check_conventions_ci_gate.py +++ b/tools/tests/test_check_conventions_ci_gate.py @@ -3,8 +3,12 @@ import subprocess import sys import tempfile import unittest +from collections import defaultdict from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from check_conventions import Check + CHECKER = Path(__file__).resolve().parents[1] / "check_conventions.py" WORKFLOWS = Path(__file__).resolve().parents[2] / ".github/workflows" @@ -21,129 +25,193 @@ def workflow(trigger, job): return f"name: Test\n\non:\n{trigger}\njobs:\n{GATE}{job}" +def wide(text): + return "\n".join(" " * (2 * (len(line) - len(line.lstrip(" ")))) + line.lstrip(" ") for line in text.split("\n")) + + PULL_REQUEST = " pull_request:\n branches: [main]\n" +TESTS = " tests:\n runs-on: ubuntu-latest\n" + +QUOTED_VALUES = ( + "{branches-ignore: ['paths:unused']}", + '{branches-ignore: ["paths-ignore:unused"]}', + "{branches-ignore: ['it''s, paths:unused']}", + r'{branches-ignore: ["escaped\", paths:unused"]}', + "{branches-ignore: ['}, paths: [core/**], {']}", +) + +QUOTED_KEYS = ( + "{branches: ['release,}'], paths: ['core/**']}", + "{'paths': ['core/**']}", + '{"paths-ignore": ["docs/**"]}', +) + +CASES = { + "job_without_gate": workflow(PULL_REQUEST, " tests:\n runs-on: ubuntu-latest\n steps:\n - run: make\n"), + "needs_without_if": workflow(PULL_REQUEST, " tests:\n needs: eligibility\n runs-on: ubuntu-latest\n"), + "if_on_another_condition": workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: github.event_name == 'pull_request'\n"), + "gated_job": workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: ${{ needs.eligibility.outputs.tests == 'true' }}\n"), + "gate_over_several_lines": workflow(PULL_REQUEST, " tests:\n needs:\n - other\n - eligibility\n if: >-\n github.event_name == 'push' ||\n needs.eligibility.outputs.tests == 'true'\n"), + "paths_filter": workflow(" pull_request:\n paths:\n - 'core/**'\n", TESTS), + "paths_of_another_trigger": workflow(" push:\n paths:\n - 'core/**'\n pull_request:\n branches: [main]\n", TESTS), + "flow_mapping_paths": workflow(" pull_request: {paths: ['core/**']}\n", TESTS), + "flow_mapping_paths_ignore": workflow(" pull_request: {paths-ignore: ['docs/**']}\n", TESTS), + "flow_mapping_without_paths": workflow(" pull_request: {branches: [main]}\n", TESTS), + **{f"quoted_value_{index}": workflow(f" pull_request: {filters}\n", TESTS) for index, filters in enumerate(QUOTED_VALUES)}, + **{f"quoted_value_{index}_top_level": f"name: Test\non: {{pull_request: {filters}}}\njobs:\n{TESTS}" + for index, filters in enumerate(QUOTED_VALUES)}, + "nested_paths": workflow(" pull_request: {branches: [{paths: ['core/**']}]}\n", TESTS), + "nested_paths_top_level": "name: Test\non: {push: {paths: ['core/**']}, pull_request: {branches: [{paths-ignore: ['docs/**']}]}}\njobs:\n" + TESTS, + **{f"quoted_key_{index}": workflow(f" pull_request: {filters}\n", TESTS) for index, filters in enumerate(QUOTED_KEYS)}, + **{f"quoted_key_{index}_top_level": f"name: Test\non: {{'pull_request': {filters}, push: {{branches: [main]}}}}\njobs:\n{TESTS}" + for index, filters in enumerate(QUOTED_KEYS)}, + "top_level_flow_paths": f"name: Test\n\non: {{pull_request: {{paths: ['core/**']}}}}\njobs:\n{GATE}{TESTS}", + "top_level_flow_paths_of_another_trigger": "name: Test\n\non: {push: {paths: ['core/**']}, pull_request: {branches: [main]}}\njobs:\n" + GATE + TESTS, + "inline_trigger_list": "name: Test\non: [push, pull_request]\njobs:\n" + TESTS, + "other_triggers": workflow(" pull_request_target:\n types: [opened]\n push:\n branches: [main]\n", TESTS), + "wide_gated": wide(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: ${{ needs.eligibility.outputs.tests == 'true' }}\n runs-on: ubuntu-latest\n")), + "wide_ungated": wide(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n runs-on: ubuntu-latest\n")), +} + + +def commit(root, files): + 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") + + +def run_checker(files): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + commit(root, files) + 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 workflow_path(case): + return f".github/workflows/{case}.yml" class CiGateTests(unittest.TestCase): - def check(self, files): - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) + @classmethod + def setUpClass(cls): + directory = tempfile.TemporaryDirectory() + cls.addClassCleanup(directory.cleanup) + root = Path(directory.name) + commit(root, {f"{case}.yml": text for case, text in CASES.items()}) + original = Path.cwd() + try: + os.chdir(root) + check = Check("HEAD~1", "HEAD") + check.workflows() + finally: + os.chdir(original) + cls.checked = set(check.status) + cls.findings = defaultdict(list) + for finding in check.findings: + cls.findings[finding[1]].append(finding) - 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, - ) + def assertGated(self, case): + self.assertIn(workflow_path(case), self.checked) + self.assertEqual(self.findings[workflow_path(case)], []) - 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 assertUngated(self, case, job): + findings = self.findings[workflow_path(case)] + self.assertIn(workflow_path(case), self.checked) + self.assertIn(job, [detail for rule, _, _, detail in findings if rule == "ci-gate"], findings) - 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_every_case_is_checked(self): + self.assertEqual(self.checked, {workflow_path(case) for case in CASES}) 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") + self.assertUngated("job_without_gate", "tests") + result = run_checker({"build.yml": CASES["job_without_gate"]}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[ci-gate]", result.stdout) + self.assertIn("tests", result.stdout) 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") + self.assertUngated("needs_without_if", "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") + self.assertUngated("if_on_another_condition", "tests") def test_gated_job_passes(self): - self.assertGated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: ${{ needs.eligibility.outputs.tests == 'true' }}\n")) + self.assertGated("gated_job") 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")) + self.assertGated("gate_over_several_lines") def test_paths_filter_passes(self): - self.assertGated(workflow(" pull_request:\n paths:\n - 'core/**'\n", " tests:\n runs-on: ubuntu-latest\n")) + self.assertGated("paths_filter") 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") + self.assertUngated("paths_of_another_trigger", "tests") def test_flow_mapping_paths_passes(self): - self.assertGated(workflow(" pull_request: {paths: ['core/**']}\n", " tests:\n runs-on: ubuntu-latest\n")) + self.assertGated("flow_mapping_paths") def test_flow_mapping_paths_ignore_passes(self): - self.assertGated(workflow(" pull_request: {paths-ignore: ['docs/**']}\n", " tests:\n runs-on: ubuntu-latest\n")) + self.assertGated("flow_mapping_paths_ignore") def test_flow_mapping_without_paths_fails(self): - self.assertUngated(workflow(" pull_request: {branches: [main]}\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + self.assertUngated("flow_mapping_without_paths", "tests") def test_flow_mapping_quoted_values_do_not_count(self): - for filters in ( - "{branches-ignore: ['paths:unused']}", - '{branches-ignore: ["paths-ignore:unused"]}', - "{branches-ignore: ['it''s, paths:unused']}", - r'{branches-ignore: ["escaped\", paths:unused"]}', - "{branches-ignore: ['}, paths: [core/**], {']}", - ): + for index, filters in enumerate(QUOTED_VALUES): with self.subTest(filters=filters): - self.assertUngated(workflow(f" pull_request: {filters}\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") - self.assertUngated(f"name: Test\non: {{pull_request: {filters}}}\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") + self.assertUngated(f"quoted_value_{index}", "tests") + self.assertUngated(f"quoted_value_{index}_top_level", "tests") def test_flow_mapping_nested_paths_do_not_count(self): - self.assertUngated(workflow(" pull_request: {branches: [{paths: ['core/**']}]}\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") - self.assertUngated("name: Test\non: {push: {paths: ['core/**']}, pull_request: {branches: [{paths-ignore: ['docs/**']}]}}\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") + self.assertUngated("nested_paths", "tests") + self.assertUngated("nested_paths_top_level", "tests") def test_flow_mapping_quoted_keys_and_delimiters_pass(self): - for filters in ( - "{branches: ['release,}'], paths: ['core/**']}", - "{'paths': ['core/**']}", - '{"paths-ignore": ["docs/**"]}', - ): + for index, filters in enumerate(QUOTED_KEYS): with self.subTest(filters=filters): - self.assertGated(workflow(f" pull_request: {filters}\n", " tests:\n runs-on: ubuntu-latest\n")) - self.assertGated(f"name: Test\non: {{'pull_request': {filters}, push: {{branches: [main]}}}}\njobs:\n tests:\n runs-on: ubuntu-latest\n") + self.assertGated(f"quoted_key_{index}") + self.assertGated(f"quoted_key_{index}_top_level") def test_top_level_flow_mapping_paths_passes(self): - self.assertGated(f"name: Test\n\non: {{pull_request: {{paths: ['core/**']}}}}\njobs:\n{GATE} tests:\n runs-on: ubuntu-latest\n") + self.assertGated("top_level_flow_paths") def test_top_level_flow_paths_of_another_trigger_do_not_count(self): - self.assertUngated("name: Test\n\non: {push: {paths: ['core/**']}, pull_request: {branches: [main]}}\njobs:\n" + GATE + " tests:\n runs-on: ubuntu-latest\n", "tests") + self.assertUngated("top_level_flow_paths_of_another_trigger", "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") + self.assertUngated("inline_trigger_list", "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")) + self.assertGated("other_triggers") def test_four_space_indentation_is_read(self): - def wide(text): - return "\n".join(" " * (2 * (len(line) - len(line.lstrip(" ")))) + line.lstrip(" ") for line in text.split("\n")) - gated = " tests:\n needs: eligibility\n if: ${{ needs.eligibility.outputs.tests == 'true' }}\n runs-on: ubuntu-latest\n" - self.assertGated(wide(workflow(PULL_REQUEST, gated))) - self.assertUngated(wide(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n runs-on: ubuntu-latest\n")), "tests") + self.assertGated("wide_gated") + self.assertUngated("wide_ungated", "tests") 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) + result = run_checker(files) self.assertNotIn("ci-gate", result.stdout, result.stdout + result.stderr) From c18ee35e528ea3f3efd9d80977b6480b4001e670 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:02:21 +0100 Subject: [PATCH 526/702] fix(relinker): reject Windows images larger than the loader maps The Windows writer accepted any image up to the 2 GiB RVA bound, so an image with a SizeOfImage above 0x77000000 converted with exit 0 and then failed to start with ERROR_BAD_EXE_FORMAT (193). WindowsPeWriter::Write now refuses such an image, matching the loader's SizeOfImage > MM_SIZE_OF_LARGEST_IMAGE (0x77000000) test, which applies to executables and to bundled modules alike. Images with a SizeOfImage of at most 0x77000000 are written unchanged. --- .../src/windows/WindowsPeWriter.cpp | 2 ++ .../tests/test_windows_sparse_load.py | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp index 123bc83ef7..af0f69328b 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp @@ -72,6 +72,8 @@ std::vector<std::uint8_t> WindowsPeWriter::Write(const std::vector<PeSection>& s } if (!foundEntry) throw Domain::RelinkerException("PE entry point is not inside executable section data", entryRva); + if (endRva > 0x77000000) + throw Domain::RelinkerException("PE image exceeds the Windows image size limit", endRva); if (fixedBase && imageBase > 0x7fffffff0000 - static_cast<std::uint64_t>(endRva)) throw Domain::RelinkerException("Fixed Windows image exceeds the user address space"); Io::WriteU32(result, optionalOffset + 4, codeSize); diff --git a/core/relinker/relinker/tests/test_windows_sparse_load.py b/core/relinker/relinker/tests/test_windows_sparse_load.py index ee7167dbbd..11f6eb9c9b 100644 --- a/core/relinker/relinker/tests/test_windows_sparse_load.py +++ b/core/relinker/relinker/tests/test_windows_sparse_load.py @@ -10,10 +10,11 @@ import tempfile from test_linux_load_alignment import PT_LOAD, fixture from test_windows_bss import LOAD_RVA, SECTION_ALIGNMENT, sections -FAR_ADDRESS = 0x7FE00000 +FAR_ADDRESS = 0x76F00000 RUNNABLE_FAR_ADDRESS = 0x4000000 ENTRY = 0x10 PEAK_MEMORY_LIMIT = 512 << 20 +WINDOWS_IMAGE_LIMIT = 0x77000000 def sparse_fixture(far_address=FAR_ADDRESS): @@ -54,6 +55,24 @@ def main(): assert raw_size == 0 and raw == 0 and rva + virtual_size <= LOAD_RVA + FAR_ADDRESS, gap far = next(section for section in table if section[2] == LOAD_RVA + FAR_ADDRESS) assert far[1] == 0x1000, far + limit_address = WINDOWS_IMAGE_LIMIT - (image_size - FAR_ADDRESS) + limit = work / "limit.elf" + limit.write_bytes(sparse_fixture(limit_address)) + limit_output = work / "limit.exe" + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(limit), str(limit_output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + limit_size = sections(limit_output.read_bytes())[1] + assert limit_size == WINDOWS_IMAGE_LIMIT, hex(limit_size) + if os.name == "nt": + executed = subprocess.run([str(limit_output)], capture_output=True, text=True, timeout=20) + assert executed.returncode == 42, f"exit code {executed.returncode:#x}" + over = work / "over.elf" + over.write_bytes(sparse_fixture(limit_address + 0x1000)) + over_output = work / "over.exe" + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(over), str(over_output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Windows image size limit" in result.stderr and not over_output.exists(), result if os.name == "nt": runnable = work / "runnable.elf" runnable.write_bytes(sparse_fixture(RUNNABLE_FAR_ADDRESS)) From b419a4a8087a656b4bcec2b460cc373ddca5ef8e Mon Sep 17 00:00:00 2001 From: simnJS <simnkrystal@gmail.com> Date: Sun, 25 Oct 2026 15:13:43 +0000 Subject: [PATCH 527/702] feat(libSceSystemService): call libSceUsbStorage from the USB storage aliases The sceSystemServiceUsbStorage* exports jump straight into the USB storage module, so call the matching libSceUsbStorage export instead of throwing. RequestMapWSB keeps its marker while its two declarations disagree on the fourth argument and the output type. --- .../prx/libSceSystemService/CMakeLists.txt | 13 +- .../libSceSystemService/PendingExports.cpp | 48 +++---- .../tests/GuestSystemServiceUsbStorage.cpp | 128 ++++++++++++++++++ docs/dev/debt/libSceSystemService.md | 2 +- 4 files changed, 156 insertions(+), 35 deletions(-) create mode 100644 core/libs/tests/GuestSystemServiceUsbStorage.cpp diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 7d8a4c65d6..9e0190d6ad 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp LoadExec.cpp) target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver) +target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver libSceUsbStorage) configure_windows_unwind(libSceSystemService) if(BUILD_TESTING) @@ -22,7 +22,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_loadexec_host) find_package(Python3 COMPONENTS Interpreter QUIET) if(Python3_FOUND) - set(loadexecLibraries $<TARGET_FILE:libSceSystemService> $<TARGET_FILE:libSceAgcDriver> $<TARGET_FILE:libc> $<TARGET_FILE:libkernel>) + set(loadexecLibraries $<TARGET_FILE:libSceSystemService> $<TARGET_FILE:libSceAgcDriver> $<TARGET_FILE:libSceUsbStorage> $<TARGET_FILE:libc> $<TARGET_FILE:libkernel>) if(APS5_ENABLE_TRACY) list(APPEND loadexecLibraries $<TARGET_FILE:TracyClient>) endif() @@ -99,6 +99,15 @@ if(BUILD_TESTING) set_tests_properties(guest_system_service_time_zone PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test_executable(guest_system_service_usb_storage_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceUsbStorage.cpp) + target_include_directories(guest_system_service_usb_storage_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_service_usb_storage_tests PRIVATE libSceSystemService libSceUsbStorage libc) + configure_windows_unwind(guest_system_service_usb_storage_tests) + add_test(NAME guest_system_service_usb_storage COMMAND guest_system_service_usb_storage_tests) + if(WIN32) + set_tests_properties(guest_system_service_usb_storage PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test_executable(guest_system_service_app_title_id_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceAppTitleId.cpp) target_include_directories(guest_system_service_app_title_id_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_service_app_title_id_tests PRIVATE libSceSystemService libkernel libc) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index bfa41fa26c..21179c6118 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -2157,58 +2157,47 @@ int APS5_VABI sceSystemServiceTurnOffScreenSaver() { } int APS5_VABI sceSystemServiceUsbStorageGetDeviceInfo(std::uint32_t device, UsbStorageDeviceInfo* info) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageGetDeviceInfo(device, info); } int APS5_VABI sceSystemServiceUsbStorageGetDeviceList(std::uint32_t* devices, std::int32_t* count) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageGetDeviceList(devices, count); } int APS5_VABI sceSystemServiceUsbStorageGetdentsClose(std::int32_t fd) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageGetdentsClose(fd); } int APS5_VABI sceSystemServiceUsbStorageGetdentsOpen(std::uint32_t device, std::int32_t* fd, std::uint64_t* bufferSize) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageGetdentsOpen(device, fd, bufferSize); } int APS5_VABI sceSystemServiceUsbStorageGetdentsRead(std::int32_t fd, void* buffer, std::uint64_t bufferSize, std::uint64_t* bytesRead) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageGetdentsRead(fd, buffer, bufferSize, bytesRead); } int APS5_VABI sceSystemServiceUsbStorageInit(void* attr) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageInit(attr); } bool APS5_VABI sceSystemServiceUsbStorageIsExist(std::uint32_t device, const char* directory, std::uint8_t* exists) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageIsExist(device, directory, exists); } int APS5_VABI sceSystemServiceUsbStorageRegisterCallback(std::int32_t eventType, UsbStorageCallback callback, void* arg) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageRegisterCallback(eventType, callback, arg); } int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapAvailable(std::uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageRegisterCallbackForMapAvailable(device, directory, callback, arg); } int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapUnavailable(std::uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageRegisterCallbackForMapUnavailable(device, directory, callback, arg); } int APS5_VABI sceSystemServiceUsbStorageRequestMap(std::uint32_t device, const char* directory, std::int32_t flags, std::uint64_t volumeSize, char* mountPoint, std::uint64_t* mountId) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageRequestMap(device, directory, flags, volumeSize, mountPoint, mountId); } int APS5_VABI sceSystemServiceUsbStorageRequestMapWSB(std::uint32_t device, const char* directory, std::int32_t flags, std::uint64_t volumeSize, char* mountPoint, std::uint64_t* mountId) { @@ -2217,28 +2206,23 @@ int APS5_VABI sceSystemServiceUsbStorageRequestMapWSB(std::uint32_t device, cons } int APS5_VABI sceSystemServiceUsbStorageRequestUnmap(std::uint32_t device, const char* directory) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageRequestUnmap(device, directory); } int APS5_VABI sceSystemServiceUsbStorageTerm() { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageTerm(); } int APS5_VABI sceSystemServiceUsbStorageUnregisterCallback(std::int32_t eventType, UsbStorageCallback callback) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageUnregisterCallback(eventType, callback); } int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapAvailable(std::uint32_t device, const char* directory, UsbStorageCallback callback) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageUnregisterCallbackForMapAvailable(device, directory, callback); } int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapUnavailable(std::uint32_t device, const char* directory, UsbStorageCallback callback) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceUsbStorageUnregisterCallbackForMapUnavailable(device, directory, callback); } int APS5_VABI sceSystemStateMgrCancelShutdownTimer() { diff --git a/core/libs/tests/GuestSystemServiceUsbStorage.cpp b/core/libs/tests/GuestSystemServiceUsbStorage.cpp new file mode 100644 index 0000000000..0c1153aca2 --- /dev/null +++ b/core/libs/tests/GuestSystemServiceUsbStorage.cpp @@ -0,0 +1,128 @@ +#include "prx/libSceSystemService/include/SystemService.hpp" +#include "prx/libSceUsbStorage/UsbStorage.hpp" +#include "prx/libSceUsbStorage/include/HostVolumes.hpp" + +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <filesystem> +#include <fstream> +#include <random> +#include <string> +#include <string_view> +#include <system_error> + +namespace { + +void APS5_VABI CallbackStub(const UsbStorageEvent*, void*) {} + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "SYSTEMSERVICE USBSTORAGE: %s\n", message); + std::abort(); + } +} + +void WriteFile(const std::filesystem::path& path, std::string_view text) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream file(path, std::ios::binary); + file << text; + file.close(); + Require(static_cast<bool>(file), "write a fake host file"); +} + +std::string Utf8(const std::filesystem::path& path) { + const auto text = path.u8string(); + return std::string(text.begin(), text.end()); +} + +void SetPassthrough(bool enabled) { +#ifdef _WIN32 + Require(_putenv_s("APS5_USB_STORAGE_PASSTHROUGH", enabled ? "1" : "") == 0, "set the passthrough switch"); +#else + Require((enabled ? ::setenv("APS5_USB_STORAGE_PASSTHROUGH", "1", 1) : ::unsetenv("APS5_USB_STORAGE_PASSTHROUGH")) == 0, "set the passthrough switch"); +#endif +} + +void TestNoDevice() { + Require(sceSystemServiceUsbStorageInit(nullptr) == 0, "initialize through the alias"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceSystemServiceUsbStorageGetDeviceList(devices, &count) == 0 && count == 0, "the alias lists no device"); + Require(sceSystemServiceUsbStorageGetDeviceList(nullptr, &count) == UsbStorage::InvalidArgument, "the alias refuses a null device list"); + UsbStorageDeviceInfo info{}; + Require(sceSystemServiceUsbStorageGetDeviceInfo(0, &info) == UsbStorage::DeviceNotFound, "the alias finds no device info"); + std::uint8_t exists = 1; + Require(sceSystemServiceUsbStorageIsExist(0, "/mnt/usb0", &exists) == true && exists == 0, "the alias reports no device"); + char mountPoint[UsbStorage::MountPointSize]; + std::uint64_t mountId = 0; + Require(sceSystemServiceUsbStorageRequestMap(0, "/", 0, 0, mountPoint, &mountId) == UsbStorage::DeviceNotFound, "the alias maps no device"); + Require(sceSystemServiceUsbStorageRequestUnmap(0, nullptr) == UsbStorage::InvalidArgument, "the alias refuses a null unmap"); + std::int32_t fd = -1; + std::uint64_t bufferSize = 0; + Require(sceSystemServiceUsbStorageGetdentsOpen(0, &fd, &bufferSize) == UsbStorage::DeviceNotFound, "the alias opens no scan"); + std::uint8_t buffer[64]; + std::uint64_t bytesRead = 1; + Require(sceSystemServiceUsbStorageGetdentsRead(1, buffer, sizeof(buffer), &bytesRead) == UsbStorage::NotSupported && bytesRead == 0, + "the alias reads no scan"); + Require(sceSystemServiceUsbStorageRegisterCallback(1, &CallbackStub, nullptr) == 0, "register a callback through the alias"); + Require(sceSystemServiceUsbStorageRegisterCallback(3, &CallbackStub, nullptr) == UsbStorage::InvalidArgument, "the alias refuses event type 3"); + Require(sceUsbStorageUnregisterCallback(1, &CallbackStub) == 0, "the alias registered with the USB storage service"); + Require(sceSystemServiceUsbStorageRegisterCallback(2, &CallbackStub, nullptr) == 0, "register an unmount callback through the alias"); + Require(sceSystemServiceUsbStorageUnregisterCallback(2, &CallbackStub) == 0, "unregister a callback through the alias"); + Require(sceSystemServiceUsbStorageUnregisterCallback(0, &CallbackStub) == UsbStorage::InvalidArgument, "the alias refuses an unregistration of event type 0"); + Require(sceSystemServiceUsbStorageRegisterCallbackForMapAvailable(0, nullptr, &CallbackStub, nullptr) == UsbStorage::InvalidArgument, + "the map-available alias refuses a null directory"); + Require(sceSystemServiceUsbStorageUnregisterCallbackForMapAvailable(0, "/", nullptr) == UsbStorage::InvalidArgument, + "the map-available alias refuses a null callback"); + Require(sceSystemServiceUsbStorageRegisterCallbackForMapUnavailable(0, nullptr, &CallbackStub, nullptr) == UsbStorage::InvalidArgument, + "the map-unavailable alias refuses a null directory"); + Require(sceSystemServiceUsbStorageUnregisterCallbackForMapUnavailable(0, "/", nullptr) == UsbStorage::InvalidArgument, + "the map-unavailable alias refuses a null callback"); + Require(sceSystemServiceUsbStorageTerm() == 0, "terminate through the alias"); +} + +void TestDevice() { + Require(sceSystemServiceUsbStorageInit(nullptr) == 0, "initialize through the alias with a device"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 1 && devices[0] == 0, "the alias initialized the USB storage service"); + UsbStorageDeviceInfo info{}; + Require(sceSystemServiceUsbStorageGetDeviceInfo(0, &info) == 0 && info.vendorId == 0x0781 && info.productId == 0x5581, "the alias reads the device info"); + std::uint8_t exists = 0; + Require(sceSystemServiceUsbStorageIsExist(0, "/", &exists) == true && exists == 1, "the alias reports the root of the device"); + char mountPoint[UsbStorage::MountPointSize]{}; + std::uint64_t mountId = 1; + Require(sceSystemServiceUsbStorageRequestMap(0, "/", 0, 0, mountPoint, &mountId) == 0, "map the device through the alias"); + Require(std::string_view(mountPoint) == "/mnt/usb0" && mountId == 0, "the alias returns the mount point"); + Require(sceSystemServiceUsbStorageRequestUnmap(0, "/mnt/usb0") == 0, "unmap the device through the alias"); + std::int32_t fd = -1; + std::uint64_t bufferSize = 0; + Require(sceSystemServiceUsbStorageGetdentsOpen(0, &fd, &bufferSize) == 0 && bufferSize == 0x8000, "open a scan through the alias"); + std::uint8_t buffer[0x100]{}; + std::uint64_t bytesRead = 0; + Require(sceSystemServiceUsbStorageGetdentsRead(fd, buffer, sizeof(buffer), &bytesRead) == 0 && bytesRead == 24, "read the scan through the alias"); + Require(std::string_view(reinterpret_cast<const char*>(buffer + 8)) == "song.txt", "the alias scan lists the device root"); + Require(sceSystemServiceUsbStorageGetdentsClose(fd) == 0, "close the scan through the alias"); + Require(sceSystemServiceUsbStorageTerm() == 0, "terminate through the alias with a device"); +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5_system_service_usb_storage_" + std::to_string(std::random_device{}())); + WriteFile(root / "drive" / "song.txt", "song"); + static const UsbStorageTestVolume none{}; + SetUsbStorageHostVolumesForTest_nid_no_patch(&none, 0); + TestNoDevice(); + const auto drive = Utf8(root / "drive"); + const UsbStorageTestVolume volume{"drive", drive.c_str(), 61530439680ull, 0x0781, 0x5581, 0x0100, "SanDisk", "Ultra Fit", "4C530001", false, false}; + SetPassthrough(true); + SetUsbStorageHostVolumesForTest_nid_no_patch(&volume, 1); + TestDevice(); + SetUsbStorageHostVolumesForTest_nid_no_patch(nullptr, 0); + std::error_code error; + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index c93bb89e5a..8879b158e4 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -155,7 +155,7 @@ - [SystemStateMgr and modal signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): keep-main-on-standby takes a string, reboot timer consumes a 32-bit mode and wake-up consumes a 16-bit trigger. Suspend/resume progress forwards a string to kernel logging. Disc readiness, shell shutdown progress and standby enablement return booleans. Modal open takes a string and close takes a 32-bit dialog ID. Platform privacy setting writes a 32-bit value; the kernel platform query it calls consumes no caller arguments. -- [USB storage aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) and [USB exports](../../../core/libs/prx/libSceUsbStorage/Export.cpp): direct-jump signatures are matched to the supplied USB module. RequestMap/RequestMapWSB take six arguments; the two extra helper arguments are internal. IsExist takes a device, directory and byte output, with a separate boolean failure result. Callbacks receive an event pointer and user data. Device information writes 824 bytes, device lists hold eight 32-bit IDs, and mount-point outputs occupy 128 bytes. Event/device field meanings and initialization attributes remain unresolved. +- [USB storage aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) and [USB exports](../../../core/libs/prx/libSceUsbStorage/Export.cpp): direct-jump signatures are matched to the supplied USB module. RequestMap/RequestMapWSB take six arguments; the two extra helper arguments are internal. IsExist takes a device, directory and byte output, with a separate boolean failure result. Callbacks receive an event pointer and user data. Device information writes 824 bytes, device lists hold eight 32-bit IDs, and mount-point outputs occupy 128 bytes. Event/device field meanings and initialization attributes remain unresolved. `sceSystemServiceUsbStorageRequestMapWSB` keeps the marker: its declaration takes `RequestMap`'s 64-bit volume size and mount-id output, while libSceUsbStorage declares shadPS4's 32-bit fourth argument and status output, and which one the module uses is unconfirmed; the other aliases call their libSceUsbStorage export. - [Assembly-only shell and clock queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): format/fsck progress returns a float in XMM0. Impose-menu, accessibility-zoom and suspend-confirmation queries take an application ID and return a boolean. Suspend-blacklist lookup forwards a 32-bit ID to SysCore. Clock changes take a string and 32-bit value; CPU pstate takes a 32-byte input structure whose fields remain unresolved, and memory pstate takes one 32-bit value. From 8130d344ea6794f6d8301235ff601a3e33e85cd3 Mon Sep 17 00:00:00 2001 From: Alexandre Figueiredo <alexfigas11@gmail.com> Date: Sun, 25 Oct 2026 23:40:37 +0000 Subject: [PATCH 528/702] fix(libc): classify x87 unnormals as denormal in _LDtest and _LDclass --- core/libs/prx/libc/src/MathExact.cpp | 9 +++++---- core/libs/tests/GuestMathExact.cpp | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/MathExact.cpp b/core/libs/prx/libc/src/MathExact.cpp index bbfe5114ef..fa9cc26643 100644 --- a/core/libs/prx/libc/src/MathExact.cpp +++ b/core/libs/prx/libc/src/MathExact.cpp @@ -42,8 +42,8 @@ short TestExtended(const ExtendedBits& bits) { const int exponent = bits.signExponent & 0x7fff; const std::uint64_t fraction = bits.mantissa & 0x7fffffffffffffffull; if (exponent == 0x7fff) return fraction != 0 ? NotANumber : Infinite; - if (exponent != 0 || (bits.mantissa >> 63) != 0) return Finite; - return bits.mantissa != 0 ? Denormal : Zero; + if ((bits.mantissa >> 63) != 0) return Finite; + return exponent != 0 || bits.mantissa != 0 ? Denormal : Zero; } template<typename TFloat> @@ -242,8 +242,9 @@ int APS5_VABI finitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0; } int APS5_VABI __fpclassifyl_nid_postfix(long double x) { const ExtendedBits bits = Bits(x); - if ((bits.signExponent & 0x7fff) != 0 || bits.mantissa == 0) return TestExtended(bits); - return Denormal; + const int exponent = bits.signExponent & 0x7fff; + if (exponent == 0) return bits.mantissa != 0 ? Denormal : Zero; + return exponent != 0x7fff ? Finite : TestExtended(bits); } int APS5_VABI __isinfl_nid_postfix(long double x) { return TestExtended(Bits(x)) == Infinite ? 1 : 0; } int APS5_VABI __isnanl_nid_postfix(long double x) { return TestExtended(Bits(x)) == NotANumber ? 1 : 0; } diff --git a/core/libs/tests/GuestMathExact.cpp b/core/libs/tests/GuestMathExact.cpp index 5db64f610d..35d1e587db 100644 --- a/core/libs/tests/GuestMathExact.cpp +++ b/core/libs/tests/GuestMathExact.cpp @@ -130,6 +130,10 @@ static void CheckRoundingModes() { static void CheckClassification() { const long double pseudoDenormal = Extended(0x0000, 0x8000000000000000ull); Require(_LDtest_nid_postfix(&pseudoDenormal) == -1 && __fpclassifyl_nid_postfix(pseudoDenormal) == -2); + for (const long double unnormal : {Extended(0x3fff, 0x4000000000000000ull), Extended(0x8001, 0), Extended(0x7ffe, 0x7fffffffffffffffull)}) { + Require(_LDtest_nid_postfix(&unnormal) == -2 && _LDclass_nid_postfix(unnormal) == -2); + Require(__fpclassifyl_nid_postfix(unnormal) == -1 && __isnormall_nid_postfix(unnormal) == 1); + } const long double zero = 0.0L, infinity = INFINITY, nan = NAN, denormal = Extended(0, 1); Require(_LDtest_nid_postfix(&zero) == 0 && _LDtest_nid_postfix(&infinity) == 1 && _LDtest_nid_postfix(&nan) == 2 && _LDtest_nid_postfix(&denormal) == -2); Require(__fpclassifyl_nid_postfix(1.0L) == -1 && __fpclassifyl_nid_postfix(0.0L) == 0 && __fpclassifyl_nid_postfix(INFINITY) == 1); From 75c9d53b28d5763f6e80ee6547a1ca4c04b0a1ea Mon Sep 17 00:00:00 2001 From: Disaster <Disvster.wav@gmail.com> Date: Tue, 27 Oct 2026 14:21:46 +0300 Subject: [PATCH 529/702] feat(libScePlayerSelectionDialog): initialize dialog parameters --- .../libScePlayerSelectionDialog/Export.cpp | 8 +++- .../include/Api.hpp | 2 +- .../libs/tests/GuestPlayerSelectionDialog.cpp | 44 ++++++++++++------- docs/dev/debt/libScePlayerSelectionDialog.md | 2 +- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp index 52b3363adb..d70f74b93b 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -79,9 +79,13 @@ int APS5_VABI scePlayerSelectionDialogGetResult(PlayerSelectionDialogResult* res return COMMON_DIALOG_RESULT_USER_CANCELED; } -int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param) { +int APS5_VABI scePlayerSelectionDialogParamInitialize(PlayerSelectionDialogParam* param) { if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - NotImplemented_nid_no_patch(__func__); + std::memset(param, 0, sizeof(*param)); + param->base.size = sizeof(param->base); + param->base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m->base)) - 1060003575u; + param->size = sizeof(*param); + return COMMON_DIALOG_OK; } int APS5_VABI scePlayerSelectionDialogTerminate(void) { diff --git a/core/libs/prx/libScePlayerSelectionDialog/include/Api.hpp b/core/libs/prx/libScePlayerSelectionDialog/include/Api.hpp index baf1617f53..8152710d3f 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/include/Api.hpp +++ b/core/libs/prx/libScePlayerSelectionDialog/include/Api.hpp @@ -11,7 +11,7 @@ int APS5_VABI scePlayerSelectionDialogClose(void); int APS5_VABI scePlayerSelectionDialogUpdateStatus(void); int APS5_VABI scePlayerSelectionDialogGetStatus(void); int APS5_VABI scePlayerSelectionDialogGetResult(PlayerSelectionDialogResult* result); -int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param); +int APS5_VABI scePlayerSelectionDialogParamInitialize(PlayerSelectionDialogParam* param); int APS5_VABI scePlayerSelectionDialogTerminate(void); } diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp index bbdf154c9d..80a87cfd4a 100644 --- a/core/libs/tests/GuestPlayerSelectionDialog.cpp +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -1,8 +1,9 @@ #include "prx/libScePlayerSelectionDialog/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include <algorithm> #include <cstdint> #include <cstdlib> -#include <stdexcept> +#include <cstring> #include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libSceUserService/UserService.hpp" @@ -21,27 +22,36 @@ constexpr int kResultUserCanceled = 1; int main() { Require(sceUserServiceInitialize(nullptr) == 0); - PlayerSelectionDialogParam param{}; - param.base.size = sizeof(param.base); - param.base.magic = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u; - param.size = sizeof(param); - param.userId = USER_SERVICE_INITIAL_USER_ID; + struct GuardedParam { + PlayerSelectionDialogParam param; + std::uint8_t guard[8]; + }; + GuardedParam guarded; + std::memset(&guarded, 0xA5, sizeof(guarded)); + auto& param = guarded.param; PlayerSelectionDialogResult result{-1, 99, nullptr, 77, 55}; Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogParamInitialize(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogParamInitialize(¶m) == 0); + Require(param.base.size == sizeof(param.base)); + Require(param.base.magic == static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(¶m)) - 1060003575u); + Require(param.size == sizeof(param)); + Require(std::all_of(std::begin(param.base.reserved), std::end(param.base.reserved), [](std::uint8_t value) { return value == 0; })); + Require(param.userId == 0); + Require(std::all_of(std::begin(param.title), std::end(param.title), [](char value) { return value == 0; })); + Require(param.maxPlayers == 0); + Require(param.mode == 0); + Require(param.padding0 == 0); + Require(param.players == nullptr); + Require(param.playerCount == 0); + Require(std::all_of(std::begin(param.reserved), std::end(param.reserved), [](std::uint8_t value) { return value == 0; })); + Require(param.padding1 == 0); + Require(std::all_of(std::begin(guarded.guard), std::end(guarded.guard), [](std::uint8_t value) { return value == 0xA5; })); + param.userId = USER_SERVICE_INITIAL_USER_ID; Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); Require(scePlayerSelectionDialogClose() == kErrNotInitialized); - Require(scePlayerSelectionDialogParamInitialize(nullptr) == kErrArgNull); - bool threw = false; - try { - scePlayerSelectionDialogParamInitialize(¶m); - - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); - Require(param.size == sizeof(param)); Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); Require(scePlayerSelectionDialogGetResult(&result) == kErrNotInitialized); Require(scePlayerSelectionDialogGetStatus() == 0); @@ -54,6 +64,8 @@ int main() { Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); Require(scePlayerSelectionDialogGetResult(&result) == kErrNotFinished); + Require(scePlayerSelectionDialogOpen(¶m) == 0); + Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); param.size = 0; Require(scePlayerSelectionDialogOpen(¶m) == static_cast<int>(0x80B8000Au)); diff --git a/docs/dev/debt/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md index 96b3ef7efc..c72b6e3940 100644 --- a/docs/dev/debt/libScePlayerSelectionDialog.md +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -6,4 +6,4 @@ ### Unknown function info -- [scePlayerSelectionDialogParamInitialize](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) (libScePlayerSelectionDialog) - returns `COMMON_DIALOG_ERROR_ARG_NULL` on a null parameter and throws `NotImplemented_nid_no_patch` for non-null pointers because `ScePlayerSelectionDialogParam` struct layout and default fields remain unverified. +- [scePlayerSelectionDialogParamInitialize](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) (libScePlayerSelectionDialog) - zeroes the parameter, sets the common dialog base size and address-derived magic, and sets the public size. The console defaults for caller-owned fields and the exact behavior when reinitializing a populated parameter remain unverified. From 007c0bf37e21c5a1bdae30b3478443c4db17ada6 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 12:52:45 +0000 Subject: [PATCH 530/702] perf(shader): allocate a program's instruction list nodes from a pool Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../include/IntermediateRepresentation/IrBlock.hpp | 11 ++++++++--- .../include/IntermediateRepresentation/IrProgram.hpp | 1 + .../IntermediateRepresentation/src/IrBlock.cpp | 8 ++++++-- .../IntermediateRepresentation/src/IrProgram.cpp | 3 ++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp index 435a5c0b1a..4c81781ca1 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp @@ -6,6 +6,7 @@ #include <array> #include <list> #include <memory> +#include <memory_resource> #include <vector> namespace ShaderRecompiler { @@ -36,17 +37,20 @@ private: std::unique_ptr<std::array<IrValue*, Size>> storage; }; +using IrInstructionList = std::pmr::list<IrValue*>; + class IrBlock { public: explicit IrBlock(std::uint32_t id); + IrBlock(std::uint32_t id, std::shared_ptr<std::pmr::memory_resource> instructionNodes); IrBlock(const IrBlock& other) = delete; IrBlock& operator=(const IrBlock& other) = delete; IrBlock(IrBlock&& other) = delete; IrBlock& operator=(IrBlock&& other) = delete; [[nodiscard]] std::uint32_t Id() const; - [[nodiscard]] std::list<IrValue*>& Instructions(); - [[nodiscard]] const std::list<IrValue*>& Instructions() const; + [[nodiscard]] IrInstructionList& Instructions(); + [[nodiscard]] const IrInstructionList& Instructions() const; [[nodiscard]] std::vector<IrBlock*>& Predecessors(); [[nodiscard]] std::vector<IrBlock*>& Successors(); @@ -69,9 +73,10 @@ public: [[nodiscard]] bool Empty() const; private: + std::shared_ptr<std::pmr::memory_resource> instructionNodes; bool ssaSealed = false; std::uint32_t id; - std::list<IrValue*> instructions; + IrInstructionList instructions; std::vector<IrBlock*> predecessors; std::vector<IrBlock*> successors; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp index 8d051b4d14..f724becd4b 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp @@ -36,6 +36,7 @@ public: [[nodiscard]] const std::vector<IrBlock*>& BlockOrder() const; private: + std::shared_ptr<std::pmr::memory_resource> instructionNodes; std::vector<std::unique_ptr<IrBlock>> blocks; std::vector<IrBlock*> blockOrder; std::vector<std::unique_ptr<IrValue>> values; diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp index 8c79b1ff6c..a47d0cf8bf 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp @@ -7,15 +7,19 @@ namespace ShaderRecompiler { IrBlock::IrBlock(std::uint32_t id) : id(id) { } +IrBlock::IrBlock(std::uint32_t id, std::shared_ptr<std::pmr::memory_resource> instructionNodes) + : instructionNodes(std::move(instructionNodes)), id(id), instructions(this->instructionNodes.get()) { +} + std::uint32_t IrBlock::Id() const { return id; } -std::list<IrValue*>& IrBlock::Instructions() { +IrInstructionList& IrBlock::Instructions() { return instructions; } -const std::list<IrValue*>& IrBlock::Instructions() const { +const IrInstructionList& IrBlock::Instructions() const { return instructions; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 4278d1a771..4c5e1fc57c 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -306,7 +306,8 @@ void IrProgram::SetWaveSize(std::uint32_t waveSize) { } IrBlock& IrProgram::CreateBlock() { - auto block = std::make_unique<IrBlock>(nextBlockId); + if (instructionNodes == nullptr) instructionNodes = std::make_shared<std::pmr::unsynchronized_pool_resource>(); + auto block = std::make_unique<IrBlock>(nextBlockId, instructionNodes); nextBlockId++; IrBlock& created = *block; blocks.push_back(std::move(block)); From 899ed0d3f2de351e0ac7896c46fa72aabd46ea20 Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Tue, 27 Oct 2026 13:59:05 +0000 Subject: [PATCH 531/702] feat(libSceNpUniversalDataSystem): declare 9 additional exports --- .../libSceNpUniversalDataSystem/Export.cpp | 38 +++++++++++++++++++ .../include/InventoryExports.hpp | 17 +++++++++ .../libSceNpUniversalDataSystem.hpp | 1 + docs/dev/debt/libSceNpUniversalDataSystem.md | 14 +++++++ 4 files changed, 70 insertions(+) create mode 100644 core/libs/prx/libSceNpUniversalDataSystem/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceNpUniversalDataSystem/Export.cpp b/core/libs/prx/libSceNpUniversalDataSystem/Export.cpp index 28b17e212b..d419958712 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/Export.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceNpUniversalDataSystem/include/InventoryExports.hpp" #include "prx/libSceNpUniversalDataSystem/include/Api.hpp" #include <cstdint> #include <cstddef> @@ -6,6 +7,43 @@ extern "C" { +int APS5_VABI sceNpUniversalDataSystemIntCloseStream() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpUniversalDataSystemIntDeleteUserData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpUniversalDataSystemIntGetNpCommIdByNpTitleId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpUniversalDataSystemIntGetNpTitleIdListByNpCommId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpUniversalDataSystemIntGetObjectState() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpUniversalDataSystemIntOpenObjectsDefinitionConfStream() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpUniversalDataSystemIntOpenObjectsDefinitionConfStreamByObjectId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpUniversalDataSystemIntOpenStreamByUri() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpUniversalDataSystemIntReadStream() { + NotImplemented_nid_no_patch(__func__); +} + + int APS5_VABI sceNpUniversalDataSystemIntAbortHandle(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceNpUniversalDataSystem/include/InventoryExports.hpp b/core/libs/prx/libSceNpUniversalDataSystem/include/InventoryExports.hpp new file mode 100644 index 0000000000..4c0a85e348 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/include/InventoryExports.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceNpUniversalDataSystemIntCloseStream(); +int APS5_VABI sceNpUniversalDataSystemIntDeleteUserData(); +int APS5_VABI sceNpUniversalDataSystemIntGetNpCommIdByNpTitleId(); +int APS5_VABI sceNpUniversalDataSystemIntGetNpTitleIdListByNpCommId(); +int APS5_VABI sceNpUniversalDataSystemIntGetObjectState(); +int APS5_VABI sceNpUniversalDataSystemIntOpenObjectsDefinitionConfStream(); +int APS5_VABI sceNpUniversalDataSystemIntOpenObjectsDefinitionConfStreamByObjectId(); +int APS5_VABI sceNpUniversalDataSystemIntOpenStreamByUri(); +int APS5_VABI sceNpUniversalDataSystemIntReadStream(); + +} diff --git a/core/libs/prx/libSceNpUniversalDataSystem/libSceNpUniversalDataSystem.hpp b/core/libs/prx/libSceNpUniversalDataSystem/libSceNpUniversalDataSystem.hpp index e72fd1c696..1a1f547f55 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/libSceNpUniversalDataSystem.hpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/libSceNpUniversalDataSystem.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceNpUniversalDataSystem/include/Api.hpp" +#include "prx/libSceNpUniversalDataSystem/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index d9602f375d..bdef7f79ef 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -2,6 +2,20 @@ ### Unknown function info +- [libSceNpUniversalDataSystem additional exports](../../../core/libs/prx/libSceNpUniversalDataSystem/Export.cpp) - these name/NID pairs are present in the 13.60 export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +| Name | NID | +| --- | --- | +| `sceNpUniversalDataSystemIntCloseStream` | `pSyd06cKZVk` | +| `sceNpUniversalDataSystemIntDeleteUserData` | `IiSeRSN++Mc` | +| `sceNpUniversalDataSystemIntGetNpCommIdByNpTitleId` | `ONOoGEhy+mg` | +| `sceNpUniversalDataSystemIntGetNpTitleIdListByNpCommId` | `FgmhJGcGztU` | +| `sceNpUniversalDataSystemIntGetObjectState` | `KSHif9MWQjQ` | +| `sceNpUniversalDataSystemIntOpenObjectsDefinitionConfStream` | `O5enyfsJoK0` | +| `sceNpUniversalDataSystemIntOpenObjectsDefinitionConfStreamByObjectId` | `vOdDBWSu-Gk` | +| `sceNpUniversalDataSystemIntOpenStreamByUri` | `tkUOy4Ag4wo` | +| `sceNpUniversalDataSystemIntReadStream` | `ABFxQ2B50RA` | + - [Events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemCreateEvent` copies `prop` and returns the event's own property object through `prop_ptr`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) does; `sceNpUniversalDataSystemEventPropertyObjectSetObject`, `SetArray` and the array setters for objects and arrays store a copy of `value` and return the stored copy through `value_ptr`. Destroying a property object or array that its Create function did not return (an event's property object or a stored copy) does nothing, as its owner frees it. These ownership rules are not verified on a console, nor are the output of `sceNpUniversalDataSystemEventToString` (still `{}`) and `sceNpUniversalDataSystemEventEstimateSize` (still 3). - [sceNpUniversalDataSystemCreateContext](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp) and [sceNpUniversalDataSystemCreateHandle](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp) (libSceNpUniversalDataSystem) - native return codes for null output pointers (`context == nullptr` and `handle == nullptr`) are unresolved and unverified on a console. The reimplementation in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp#L620-L640) returns `0x80553102` (`SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT`), but this error return is unverified against native hardware; host paths continue to throw `std::invalid_argument` until the native contract is established. From 9fc3a5aa594e0c7541cd60a3a5cd60d231efb916 Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Tue, 27 Oct 2026 13:59:05 +0000 Subject: [PATCH 532/702] feat(libSceRemoteplay): declare 4 additional exports --- core/libs/prx/libSceRemoteplay/Export.cpp | 18 ++++++++++++++++++ .../include/InventoryExports.hpp | 12 ++++++++++++ .../prx/libSceRemoteplay/libSceRemoteplay.hpp | 1 + docs/dev/debt/libSceRemoteplay.md | 9 +++++++++ 4 files changed, 40 insertions(+) create mode 100644 core/libs/prx/libSceRemoteplay/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceRemoteplay/Export.cpp b/core/libs/prx/libSceRemoteplay/Export.cpp index c398ec2033..fbc5a0351f 100644 --- a/core/libs/prx/libSceRemoteplay/Export.cpp +++ b/core/libs/prx/libSceRemoteplay/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceRemoteplay/include/InventoryExports.hpp" #include "prx/libSceRemoteplay/include/Api.hpp" #include <cstdint> #include <cstddef> @@ -10,6 +11,23 @@ constexpr int REMOTEPLAY_CONNECTION_STATUS_DISCONNECT = 0; extern "C" { +int APS5_VABI sceRemoteplayIsRemoteViewering() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemoteplayNotifyCanWakeup() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemoteplayRegistCallback() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemoteplayUnregistCallback() { + NotImplemented_nid_no_patch(__func__); +} + + int APS5_VABI sceRemoteplayGetConnectionStatus(int user_id, int* status) { (void)user_id; if (!status) APS5_INVALID_ARG_EX; diff --git a/core/libs/prx/libSceRemoteplay/include/InventoryExports.hpp b/core/libs/prx/libSceRemoteplay/include/InventoryExports.hpp new file mode 100644 index 0000000000..8b7845baef --- /dev/null +++ b/core/libs/prx/libSceRemoteplay/include/InventoryExports.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceRemoteplayIsRemoteViewering(); +int APS5_VABI sceRemoteplayNotifyCanWakeup(); +int APS5_VABI sceRemoteplayRegistCallback(); +int APS5_VABI sceRemoteplayUnregistCallback(); + +} diff --git a/core/libs/prx/libSceRemoteplay/libSceRemoteplay.hpp b/core/libs/prx/libSceRemoteplay/libSceRemoteplay.hpp index a04f3f394f..9bf2668fcb 100644 --- a/core/libs/prx/libSceRemoteplay/libSceRemoteplay.hpp +++ b/core/libs/prx/libSceRemoteplay/libSceRemoteplay.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceRemoteplay/include/Api.hpp" +#include "prx/libSceRemoteplay/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceRemoteplay.md b/docs/dev/debt/libSceRemoteplay.md index 7435fe1106..d25434e10c 100644 --- a/docs/dev/debt/libSceRemoteplay.md +++ b/docs/dev/debt/libSceRemoteplay.md @@ -6,6 +6,15 @@ ### Unknown function info +- [libSceRemoteplay additional exports](../../../core/libs/prx/libSceRemoteplay/Export.cpp) - these name/NID pairs are present in the 13.60 export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +| Name | NID | +| --- | --- | +| `sceRemoteplayIsRemoteViewering` | `PBhv2eFxDDs` | +| `sceRemoteplayNotifyCanWakeup` | `KHxNaWydXss` | +| `sceRemoteplayRegistCallback` | `pv4ZWTRweyI` | +| `sceRemoteplayUnregistCallback` | `BHUdZh6st9c` | + - [sceRemoteplayApprove](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) - [sceRemoteplayClearAllRegistData](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) - [sceRemoteplayClearConnectHistory](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) From 0962de20d86c0aa57dd753490d5a7f4b9ee65537 Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Tue, 27 Oct 2026 13:59:06 +0000 Subject: [PATCH 533/702] feat(libSceContentSearch): declare 4 additional exports --- core/libs/prx/libSceContentSearch/Export.cpp | 18 ++++++++++++++++++ .../include/InventoryExports.hpp | 12 ++++++++++++ .../libSceContentSearch.hpp | 1 + docs/dev/debt/libSceContentSearch.md | 9 +++++++++ 4 files changed, 40 insertions(+) create mode 100644 core/libs/prx/libSceContentSearch/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index 10c13bfbe1..3b3229ac06 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceContentSearch/include/InventoryExports.hpp" #include "prx/libSceContentSearch/include/Api.hpp" #include <cstdint> #include <cstddef> @@ -62,6 +63,23 @@ void ValidateOrderBy(const char* funcName, const ContentSearchContentOrderByCond extern "C" { +int APS5_VABI sceContentSearchAddAvContentCreatedEventCondition() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceContentSearchDisableAvContentCreatedEvent() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceContentSearchEnableAvContentCreatedEvent() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceContentSearchGetContentIdByAvContentCreatedEvent() { + NotImplemented_nid_no_patch(__func__); +} + + int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param) { if (g_initialized.load()) return ErrorAlreadyInitialized; if (init_param == nullptr || init_param->memory_size == 0 || init_param->memory_size % 16384 != 0) return ErrorInvalidArgument; diff --git a/core/libs/prx/libSceContentSearch/include/InventoryExports.hpp b/core/libs/prx/libSceContentSearch/include/InventoryExports.hpp new file mode 100644 index 0000000000..b5a22ec35d --- /dev/null +++ b/core/libs/prx/libSceContentSearch/include/InventoryExports.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceContentSearchAddAvContentCreatedEventCondition(); +int APS5_VABI sceContentSearchDisableAvContentCreatedEvent(); +int APS5_VABI sceContentSearchEnableAvContentCreatedEvent(); +int APS5_VABI sceContentSearchGetContentIdByAvContentCreatedEvent(); + +} diff --git a/core/libs/prx/libSceContentSearch/libSceContentSearch.hpp b/core/libs/prx/libSceContentSearch/libSceContentSearch.hpp index 79d8740064..1398ef35d4 100644 --- a/core/libs/prx/libSceContentSearch/libSceContentSearch.hpp +++ b/core/libs/prx/libSceContentSearch/libSceContentSearch.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceContentSearch/include/Api.hpp" +#include "prx/libSceContentSearch/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceContentSearch.md b/docs/dev/debt/libSceContentSearch.md index fe7b25931c..ccb59cc6a2 100644 --- a/docs/dev/debt/libSceContentSearch.md +++ b/docs/dev/debt/libSceContentSearch.md @@ -2,6 +2,15 @@ ### Unknown function info +- [libSceContentSearch additional exports](../../../core/libs/prx/libSceContentSearch/Export.cpp) - these name/NID pairs are present in the 13.60 export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +| Name | NID | +| --- | --- | +| `sceContentSearchAddAvContentCreatedEventCondition` | `HzLPPnuWTek` | +| `sceContentSearchDisableAvContentCreatedEvent` | `cb8Dl69jk7A` | +| `sceContentSearchEnableAvContentCreatedEvent` | `vlOqhFPOLDs` | +| `sceContentSearchGetContentIdByAvContentCreatedEvent` | `2Dj56xvkS5M` | + - [sceContentBinderEnd, sceContentBinderGetNumOfDifference, sceContentBinderInitialize, sceContentBinderIsUpdated, sceContentBinderReset, sceContentBinderSearchDifference, sceContentBinderStart, sceContentBinderTerminate, sceContentSearchGetApplicationLastUpdateId, sceContentSearchGetMyApplicationIndex, sceContentSearchGetNumOfApplication and sceContentSearchSearchApplication](../../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceContentSearch.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. ### Functional From d9b9cb6b799257866fee6194c89ffcfb29f6e408 Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:06:17 +0000 Subject: [PATCH 534/702] test(relinker): pin annotate_module against the guest module writer --- core/relinker/CMakeLists.txt | 2 + .../tests/test_annotate_guest_module.py | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 core/relinker/relinker/tests/test_annotate_guest_module.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2267be074f..4065811fdb 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -671,6 +671,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executables.py $<TARGET_FILE:relinker>) add_test(NAME game_patches COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_game_patches.py $<TARGET_FILE:relinker>) + add_test(NAME annotate_guest_module COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_annotate_guest_module.py $<TARGET_FILE:relinker>) if(Python3_VERSION VERSION_GREATER_EQUAL 3.11) add_test(NAME patch_advisor_unit COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/tests/test_suggest_patches.py) diff --git a/core/relinker/relinker/tests/test_annotate_guest_module.py b/core/relinker/relinker/tests/test_annotate_guest_module.py new file mode 100644 index 0000000000..03f39beed0 --- /dev/null +++ b/core/relinker/relinker/tests/test_annotate_guest_module.py @@ -0,0 +1,53 @@ +import subprocess +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[4] / "tools")) +import annotate_module + +from test_guest_intel_trampolines import main_fixture +from test_guest_none_relocations import JUMP_SLOT, module + + +def convert(root, rela, plt): + (root / "in" / "sce_module").mkdir(parents=True) + (root / "in" / "sce_module" / "module.prx").write_bytes(bytes(module(rela, plt))) + (root / "in" / "eboot.bin").write_bytes(bytes(main_fixture())) + (root / "out").mkdir() + result = subprocess.run([sys.argv[1], str(root / "in" / "eboot.bin"), str(root / "out" / "app.elf")], + capture_output=True, text=True, timeout=120) + assert result.returncode == 0, result.stderr + artifacts = sorted((root / "out").rglob("*.guest.prx")) + assert len(artifacts) == 1, artifacts + return artifacts[0] + + +def parsed(path): + return annotate_module.Module(path.read_bytes()) + + +def main(): + with tempfile.TemporaryDirectory(prefix="anyps5-annotate-guest-") as directory: + root = Path(directory) + + imported = parsed(convert(root / "imports", [], [JUMP_SLOT])) + assert imported.symbol_count >= 2, imported.symbol_count + slots = imported.imports() + assert len(slots) == 1, slots + assert "shared" in list(slots.values())[0], slots + exports, thread_local = imported.exports() + assert len(exports) == 1 and not thread_local, (exports, thread_local) + assert "shared" in list(exports.values())[0], exports + assert imported.code_segments(), "no code segments" + + plain = parsed(convert(root / "plain", [], [])) + assert plain.imports() == {}, plain.imports() + exports, thread_local = plain.exports() + assert len(exports) == 1 and not thread_local, (exports, thread_local) + assert plain.code_segments(), "no code segments" + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 953704f6e2a211c0e0be4b119fd995aadca6e00a Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:37:46 +0530 Subject: [PATCH 535/702] test(tools): skip annotate_module tests when GNU objdump is unavailable --- tools/tests/test_annotate_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_annotate_module.py b/tools/tests/test_annotate_module.py index bb01a2b717..43a400825b 100644 --- a/tools/tests/test_annotate_module.py +++ b/tools/tests/test_annotate_module.py @@ -120,7 +120,8 @@ class ObjdumpDiscoveryTests(unittest.TestCase): class AnnotateModuleTests(unittest.TestCase): def setUp(self): - self.assertIsNotNone(annotate_module.gnu_objdump(), "GNU binutils objdump (objdump or gobjdump) is required to run these tests") + if annotate_module.gnu_objdump() is None: + self.skipTest("GNU binutils objdump (objdump or gobjdump) is required to run these tests") self.directory = tempfile.TemporaryDirectory() self.addCleanup(self.directory.cleanup) self.root = Path(self.directory.name) From 2d5d30c099c47ee9b4deed6afabf57aa281359e3 Mon Sep 17 00:00:00 2001 From: janzezno <jan.masa889@gmail.com> Date: Tue, 27 Oct 2026 15:10:21 +0100 Subject: [PATCH 536/702] fix(agc): pick the overlay's palette entry with constant indices again --- .../libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag index f9db234586..9bcbd62e64 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag @@ -8,5 +8,5 @@ layout(location = 0) flat in uint paletteIndex; layout(location = 0) out vec4 color; void main() { - color = paletteIndex < 7u ? parameters.palette[paletteIndex] : parameters.palette[0]; + color = paletteIndex == 1u ? parameters.palette[1] : paletteIndex == 2u ? parameters.palette[2] : paletteIndex == 3u ? parameters.palette[3] : paletteIndex == 4u ? parameters.palette[4] : paletteIndex == 5u ? parameters.palette[5] : paletteIndex == 6u ? parameters.palette[6] : parameters.palette[0]; } From 17703bd1b718dc7f4b852c3eb70092b12bb85905 Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Tue, 27 Oct 2026 14:14:55 +0000 Subject: [PATCH 537/702] feat(libSceAppContent): declare 4 additional exports --- core/libs/prx/libSceAppContent/Export.cpp | 18 ++++++++++++++++++ .../include/InventoryExports.hpp | 12 ++++++++++++ .../prx/libSceAppContent/libSceAppContent.hpp | 2 ++ docs/dev/debt/libSceAppContent.md | 9 +++++++++ 4 files changed, 41 insertions(+) create mode 100644 core/libs/prx/libSceAppContent/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 96a00cca78..478f72de02 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAppContent/include/InventoryExports.hpp" #include "prx/libSceAppContent/include/Api.hpp" #include <cstdint> #include <cstddef> @@ -69,6 +70,23 @@ static void ClearDirectory(const std::filesystem::path& directory) { extern "C" { +int APS5_VABI sceAppContentCheckBundleLicenseOnDisc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAppContentDownloadDataGetBlockSize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAppContentGetGameTrialsFlag() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAppContentGetPlayableStatus() { + NotImplemented_nid_no_patch(__func__); +} + + int APS5_VABI sceAppContentAddcontMount(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentMountPoint* mount_point) { if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; diff --git a/core/libs/prx/libSceAppContent/include/InventoryExports.hpp b/core/libs/prx/libSceAppContent/include/InventoryExports.hpp new file mode 100644 index 0000000000..2c7f64dfb8 --- /dev/null +++ b/core/libs/prx/libSceAppContent/include/InventoryExports.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceAppContentCheckBundleLicenseOnDisc(); +int APS5_VABI sceAppContentDownloadDataGetBlockSize(); +int APS5_VABI sceAppContentGetGameTrialsFlag(); +int APS5_VABI sceAppContentGetPlayableStatus(); + +} diff --git a/core/libs/prx/libSceAppContent/libSceAppContent.hpp b/core/libs/prx/libSceAppContent/libSceAppContent.hpp index d92b11cf65..0d4428540d 100644 --- a/core/libs/prx/libSceAppContent/libSceAppContent.hpp +++ b/core/libs/prx/libSceAppContent/libSceAppContent.hpp @@ -1,3 +1,5 @@ #pragma once #include "prx/libSceAppContent/include/Api.hpp" + +#include "prx/libSceAppContent/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index d0daef9004..c75f621a36 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -2,6 +2,15 @@ ### Unknown function info +- [libSceAppContent additional exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - these name/NID pairs are present in the 13.60 export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +| Name | NID | +| --- | --- | +| `sceAppContentCheckBundleLicenseOnDisc` | `xZo2-418Wdo` | +| `sceAppContentDownloadDataGetBlockSize` | `9ht66cHTj7g` | +| `sceAppContentGetGameTrialsFlag` | `1saJukIkcKw` | +| `sceAppContentGetPlayableStatus` | `y8meQn-Qy5c` | + - [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB. One disassembled title (PPSA15246) calls it with the `/download0` mount point and a zeroed 64-bit output, ignores the return value and multiplies the output by 1024, so for that title an error that leaves the output untouched and an OK with 0 KB look the same - [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified From 7853f0fbc625c679dafe8acee53a7587129f747e Mon Sep 17 00:00:00 2001 From: Kyon <kyonosuke.eth@gmail.com> Date: Tue, 27 Oct 2026 14:14:56 +0000 Subject: [PATCH 538/702] feat(libSceSystemLogger2Game): declare 2 additional exports --- core/libs/prx/libSceSystemLogger2Game/Export.cpp | 10 ++++++++++ .../include/InventoryExports.hpp | 10 ++++++++++ .../libSceSystemLogger2Game.hpp | 2 ++ docs/dev/debt/libSceSystemLogger2Game.md | 10 ++++++++++ 4 files changed, 32 insertions(+) create mode 100644 core/libs/prx/libSceSystemLogger2Game/include/InventoryExports.hpp create mode 100644 docs/dev/debt/libSceSystemLogger2Game.md diff --git a/core/libs/prx/libSceSystemLogger2Game/Export.cpp b/core/libs/prx/libSceSystemLogger2Game/Export.cpp index d81f4a5e32..c7f642733d 100644 --- a/core/libs/prx/libSceSystemLogger2Game/Export.cpp +++ b/core/libs/prx/libSceSystemLogger2Game/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceSystemLogger2Game/include/InventoryExports.hpp" #include <cstddef> #include <cstdint> #include "SceTypes.hpp" @@ -5,6 +6,15 @@ extern "C" { +int APS5_VABI sceSystemLogger2GameGetTimestamp() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceSystemLogger2GameSetEvent() { + NotImplemented_nid_no_patch(__func__); +} + + APS5_DUMMY_FUN } diff --git a/core/libs/prx/libSceSystemLogger2Game/include/InventoryExports.hpp b/core/libs/prx/libSceSystemLogger2Game/include/InventoryExports.hpp new file mode 100644 index 0000000000..d91ba52862 --- /dev/null +++ b/core/libs/prx/libSceSystemLogger2Game/include/InventoryExports.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceSystemLogger2GameGetTimestamp(); +int APS5_VABI sceSystemLogger2GameSetEvent(); + +} diff --git a/core/libs/prx/libSceSystemLogger2Game/libSceSystemLogger2Game.hpp b/core/libs/prx/libSceSystemLogger2Game/libSceSystemLogger2Game.hpp index 6f70f09bee..7948b1dc9d 100644 --- a/core/libs/prx/libSceSystemLogger2Game/libSceSystemLogger2Game.hpp +++ b/core/libs/prx/libSceSystemLogger2Game/libSceSystemLogger2Game.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceSystemLogger2Game/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceSystemLogger2Game.md b/docs/dev/debt/libSceSystemLogger2Game.md new file mode 100644 index 0000000000..7660ae6825 --- /dev/null +++ b/docs/dev/debt/libSceSystemLogger2Game.md @@ -0,0 +1,10 @@ +# libSceSystemLogger2Game technical debt + +### Unknown function info + +- [libSceSystemLogger2Game additional exports](../../../core/libs/prx/libSceSystemLogger2Game/Export.cpp) - these name/NID pairs are present in the 13.60 export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +| Name | NID | +| --- | --- | +| `sceSystemLogger2GameGetTimestamp` | `ZNlFA5YKy4A` | +| `sceSystemLogger2GameSetEvent` | `AAqtvm0YEss` | From 802d506d1a07fb3030c38e9f3924217436d61bd8 Mon Sep 17 00:00:00 2001 From: Mohamed Malek Aifa <32399935+malekaifa@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:27:37 +0100 Subject: [PATCH 539/702] fix(tools): report invalid web archive encoding --- tools/test_web.py | 2 +- tools/tests/test_web_release.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/test_web.py b/tools/test_web.py index 03fe29a0c7..bded99c97f 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -45,7 +45,7 @@ def main(argv=None): subprocess.run(["node", str(ROOT / "demo/web/tests/archive-root.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) subprocess.run(["node", str(ROOT / "demo/web/tests/archive-windows.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) - except (subprocess.CalledProcessError, urllib.error.URLError, OSError) as error: + except (subprocess.CalledProcessError, urllib.error.URLError, OSError, UnicodeDecodeError) as error: parser.error(str(error)) return 0 diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index f33e48e6a5..0e7dae3d10 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -476,6 +476,18 @@ class TestWebRunnerTests(unittest.TestCase): self.assertEqual(error.exception.code, 2) self.assertIn("zip library URL not found", err.getvalue()) + def test_main_rejects_invalid_utf8_archive_without_traceback(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + (build / "web").mkdir(parents=True) + (build / "web/archive.mjs").write_bytes(b"\xff\xfe\x80") + with patch("subprocess.run"): + with self.assertRaises(SystemExit) as error, contextlib.redirect_stderr(io.StringIO()) as err: + test_web.main(["--build", str(build)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("can't decode byte", err.getvalue()) + self.assertNotIn("Traceback", err.getvalue()) + def test_main_decodes_archive_with_utf8_bom(self): with tempfile.TemporaryDirectory() as tmp: build = Path(tmp) / "build" From 7c7cc70f2e443d2f20e6afe1209a96b3933f716a Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 27 Oct 2026 10:28:10 -0400 Subject: [PATCH 540/702] fix(libc): copy count units of a strxfrm or wcsxfrm result that does not fit, as titles' libc.prx does When the transformed string does not fit, strxfrm and wcsxfrm wrote count - 1 characters and a terminator. The libc.prx bundled with titles copies the first count characters and writes no terminator, as a plain copy of count units would, and copies the terminator only when the whole result fits. Both functions now do the same; the return value, the length of the source, is unchanged. The tests that expected the terminated prefix expect the measured units instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Strings.cpp | 10 ++++------ core/libs/tests/GuestStrings.cpp | 6 +++--- core/libs/tests/GuestWideStrings.cpp | 10 +++++----- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 128c85cd7e..db6ca64fdc 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -830,16 +830,14 @@ int APS5_VABI wcscoll_nid_postfix(const char16_t* first, const char16_t* second) size_t APS5_VABI wcsxfrm_nid_postfix(char16_t* destination, const char16_t* source, size_t count) { const size_t length = Length(source); - if (count != 0) { - const size_t copied = length < count ? length : count - 1; - std::memcpy(destination, source, copied * sizeof(char16_t)); - destination[copied] = 0; - } + if (count != 0) std::memcpy(destination, source, (length < count ? length + 1 : count) * sizeof(char16_t)); return length; } size_t APS5_VABI strxfrm_nid_postfix(char* destination, const char* source, size_t count) { - return strlcpy_nid_postfix(destination, source, count); + const size_t length = std::strlen(source); + if (count != 0) std::memcpy(destination, source, length < count ? length + 1 : count); + return length; } char* APS5_VABI strtok_s_nid_postfix(char* str, const char* delim, char** context) { diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index c7ee8ee32e..35d1097655 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -422,14 +422,14 @@ int main() { Require(std::strcmp(transformed, "abc") == 0 && transformed[4] == 'z'); std::memset(transformed, 'z', sizeof(transformed)); Require(strxfrm_nid_postfix(transformed, "abcdefgh", 4) == 8); - Require(std::strcmp(transformed, "abc") == 0 && transformed[4] == 'z'); + Require(std::memcmp(transformed, "abcd", 4) == 0 && transformed[4] == 'z'); std::memset(transformed, 'z', sizeof(transformed)); Require(strxfrm_nid_postfix(transformed, "abcdefg", sizeof(transformed)) == 7 && std::strcmp(transformed, "abcdefg") == 0); std::memset(transformed, 'z', sizeof(transformed)); - Require(strxfrm_nid_postfix(transformed, "abcdefgh", sizeof(transformed)) == 8 && std::strcmp(transformed, "abcdefg") == 0); + Require(strxfrm_nid_postfix(transformed, "abcdefgh", sizeof(transformed)) == 8 && std::memcmp(transformed, "abcdefgh", 8) == 0); std::memset(transformed, 'z', sizeof(transformed)); Require(strxfrm_nid_postfix(transformed, "", sizeof(transformed)) == 0 && transformed[0] == '\0' && transformed[1] == 'z'); - Require(strxfrm_nid_postfix(transformed, "abc", 1) == 3 && transformed[0] == '\0' && transformed[1] == 'z'); + Require(strxfrm_nid_postfix(transformed, "abc", 1) == 3 && transformed[0] == 'a' && transformed[1] == 'z'); Require(strxfrm_nid_postfix(nullptr, "abc", 0) == 3); Require(strpbrk_nid_postfix(buffer, "ay") == buffer + 1); Require(strpbrk_nid_postfix(buffer, "") == nullptr); diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index e31bb8ed9b..620b1a62c3 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -360,8 +360,8 @@ int main() { wmemset_nid_postfix(transformed, 0xaaaa, 8); require(wcsxfrm_nid_postfix(transformed, u"much too long", 4) == 13); - if (!same(transformed, u"muc", 4)) std::fputs("wcsxfrm did not copy and terminate its truncated result\n", stderr); - require(same(transformed, u"muc", 4) && transformed[4] == 0xaaaa); + if (!same(transformed, u"much", 4)) std::fputs("wcsxfrm did not copy the first count units of its truncated result\n", stderr); + require(same(transformed, u"much", 4) && transformed[4] == 0xaaaa); require(wcsxfrm_nid_postfix(nullptr, u"abc", 0) == 3); struct TransformCase { const char16_t* source; @@ -372,13 +372,13 @@ int main() { }; for (const TransformCase test : { TransformCase{u"wide", 0, 4, u"", 0}, - TransformCase{u"wide", 1, 4, u"", 1}, - TransformCase{u"wide", 4, 4, u"wid", 4}, + TransformCase{u"wide", 1, 4, u"w", 1}, + TransformCase{u"wide", 4, 4, u"wide", 4}, TransformCase{u"wide", 5, 4, u"wide", 5}, TransformCase{u"", 0, 0, u"", 0}, TransformCase{u"", 1, 0, u"", 1}, TransformCase{u"", 8, 0, u"", 1}, - TransformCase{u"\x7fff\x8000\xffff\xd83d\xde00", 4, 5, u"\x7fff\x8000\xffff", 4}, + TransformCase{u"\x7fff\x8000\xffff\xd83d\xde00", 4, 5, u"\x7fff\x8000\xffff\xd83d", 4}, TransformCase{u"\x7fff\x8000\xffff\xd83d\xde00", 6, 5, u"\x7fff\x8000\xffff\xd83d\xde00", 6} }) { wmemset_nid_postfix(transformed, 0xaaaa, 8); From 8c12ea0515989096976a9cc7ed8c11118f5791a4 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:12:45 +0530 Subject: [PATCH 541/702] test(libSceWebBrowserDialog): cover post-terminate cookie operations --- core/libs/tests/GuestWebBrowserDialogCookies.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestWebBrowserDialogCookies.cpp b/core/libs/tests/GuestWebBrowserDialogCookies.cpp index 32deb3db57..abaa142bc3 100644 --- a/core/libs/tests/GuestWebBrowserDialogCookies.cpp +++ b/core/libs/tests/GuestWebBrowserDialogCookies.cpp @@ -120,6 +120,12 @@ int main() { CheckResetCookieParameters(); CheckOpenDialog(); Require(sceWebBrowserDialogTerminate() == 0, "final Terminate"); + Require(sceWebBrowserDialogTerminate() == NOT_INITIALIZED, "Terminate when uninitialized"); + Require(sceWebBrowserDialogSetCookie(&cookie) == NOT_INITIALIZED, "SetCookie after Terminate"); + Require(sceWebBrowserDialogSetCookie(nullptr) == NOT_INITIALIZED, "SetCookie null after Terminate"); + Require(sceWebBrowserDialogResetCookie(&reset) == NOT_INITIALIZED, "ResetCookie after Terminate"); + Require(sceWebBrowserDialogResetCookie(nullptr) == NOT_INITIALIZED, "ResetCookie null after Terminate"); + Require(sceWebBrowserDialogGetStatus() == 0, "GetStatus after Terminate"); std::puts("guest web browser dialog cookies: PASS"); return 0; } From 458da3450e785cac03398cdc3a7ebe7ec2af684a Mon Sep 17 00:00:00 2001 From: Matheus Herberts <163153928+Mherberts@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:25:16 -0300 Subject: [PATCH 542/702] feat(relinker): support packed ELF64 relative relocations --- core/relinker/CMakeLists.txt | 12 ++ .../relinker/parsing/RelrRelocations.hpp | 17 ++ .../RelativeRelocationIndex.cpp | 13 +- .../relinker/src/guest/GuestImageReader.cpp | 14 +- .../relinker/src/parsing/RelrRelocations.cpp | 94 +++++++++ .../src/pipeline/RelinkerPipeline.cpp | 14 +- .../relinker/tests/RelrRelocationTests.cpp | 123 ++++++++++++ .../relinker/tests/test_optional_rela.py | 4 +- .../relinker/tests/test_relr_relocations.py | 179 ++++++++++++++++++ docs/dev/debt/relinker.md | 2 + 10 files changed, 467 insertions(+), 5 deletions(-) create mode 100644 core/relinker/relinker/include/relinker/parsing/RelrRelocations.hpp create mode 100644 core/relinker/relinker/src/parsing/RelrRelocations.cpp create mode 100644 core/relinker/relinker/tests/RelrRelocationTests.cpp create mode 100644 core/relinker/relinker/tests/test_relr_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2267be074f..451ef4eade 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -56,6 +56,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/MaskedStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/RelrRelocations.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CodeInstructionCollector.cpp @@ -242,6 +243,11 @@ 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(relr_relocations_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/RelrRelocationTests.cpp) + target_link_libraries(relr_relocations_tests PRIVATE relinker_core) + add_test(NAME relr_relocations COMMAND relr_relocations_tests) + set_tests_properties(relr_relocations 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 @@ -806,6 +812,11 @@ if(BUILD_TESTING) endif() add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $<TARGET_FILE:relinker>) + add_test(NAME relr_conversion COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_relr_relocations.py $<TARGET_FILE:relinker>) + add_test(NAME guest_relr_conversion COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_relr_relocations.py $<TARGET_FILE:relinker> guest) + set_tests_properties(relr_conversion guest_relr_conversion PROPERTIES TIMEOUT 20) add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_weak_imports.py $<TARGET_FILE:relinker>) add_test(NAME windows_guest_weak_imports COMMAND ${Python3_EXECUTABLE} @@ -931,6 +942,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp ) target_sources(strict_nid_filter_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/RelrRelocations.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp diff --git a/core/relinker/relinker/include/relinker/parsing/RelrRelocations.hpp b/core/relinker/relinker/include/relinker/parsing/RelrRelocations.hpp new file mode 100644 index 0000000000..c65d433b33 --- /dev/null +++ b/core/relinker/relinker/include/relinker/parsing/RelrRelocations.hpp @@ -0,0 +1,17 @@ +#ifndef RELINKER_PARSING_RELRRELOCATIONS_HPP +#define RELINKER_PARSING_RELRRELOCATIONS_HPP + +#include <domain/Types.hpp> +#include <map> +#include <span> + +namespace Relinker { + +std::map<Domain::VirtualAddress, std::uint64_t> ReadRelrRelocations( + std::span<const std::uint8_t> bytes, + std::span<const Domain::ProgramHeader> headers, + std::span<const Domain::DynamicTag> tags); + +} + +#endif diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp index 9108445515..ad534c1ddc 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -1,5 +1,6 @@ #include <relinker/analysis/UnusedNidFilter/IRelativeRelocationIndex.hpp> #include <relinker/parsing/ElfReader.hpp> +#include <relinker/parsing/RelrRelocations.hpp> #include <elfpatcher/general/ElfConstants.hpp> #include <unordered_map> #include <cstring> @@ -155,7 +156,8 @@ std::unique_ptr<IRelativeRelocationIndex> BuildRelativeRelocationIndex( sceTableBase = read64(elfBytes, phPos + 8); } } - for (const auto& header : ElfReader(elfBytes).ReadProgramHeaders()) { + const auto headers = ElfReader(elfBytes).ReadProgramHeaders(); + for (const auto& header : headers) { if (header.Type != PT_LOAD) continue; loads.push_back({header.MappedAddress, header.Offset, header.FileSize}); } @@ -189,6 +191,7 @@ std::unique_ptr<IRelativeRelocationIndex> BuildRelativeRelocationIndex( bool hasSysvPltrelsz = false; bool hasOsPltrelsz = false; bool terminated = false; + std::vector<Domain::DynamicTag> relrTags; for (std::uint64_t off = 0; off <= segSz && segSz - off >= 16; off += 16) { std::size_t pos = static_cast<std::size_t>(segOff + off); @@ -199,6 +202,7 @@ std::unique_ptr<IRelativeRelocationIndex> BuildRelativeRelocationIndex( terminated = true; break; } + if (tag == DT_RELR || tag == DT_RELRSZ || tag == DT_RELRENT) relrTags.push_back({tag, val}); if (tag == DT_RELA) { if (hasOsRela) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELA"); if (hasSysvRela && relaVa.value() != val) throw RelinkerException("Conflicting duplicate DT_RELA tags"); @@ -265,6 +269,13 @@ std::unique_ptr<IRelativeRelocationIndex> BuildRelativeRelocationIndex( collectRelativeEntries(elfBytes, loads, *relaVa, *relaSz, hasOsRela, sceTableBase, targets); if (jmprelVa.has_value()) collectRelativeEntries(elfBytes, loads, *jmprelVa, *pltrelsz, hasOsJmprel, sceTableBase, targets); + for (const auto& [target, addend] : ReadRelrRelocations(elfBytes, headers, relrTags)) { + if (addend > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) + throw RelinkerException("Relative relocation target address is negative", target); + const auto [found, inserted] = targets.emplace(target, addend); + if (!inserted && found->second != addend) + throw RelinkerException("Conflicting relative relocation targets for slot", target); + } } return std::make_unique<RelativeRelocationIndex>(std::move(targets)); diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 9e058f2b6e..fda69cf24b 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -1,5 +1,6 @@ #include <relinker/guest/GuestImage.hpp> #include <relinker/parsing/ElfReader.hpp> +#include <relinker/parsing/RelrRelocations.hpp> #include <io/BufferUtils.hpp> #include <io/NativePath.hpp> #include <algorithm> @@ -83,7 +84,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } } if (!terminated) fail("Unterminated dynamic segment"); - for (const auto unsupported : {15u, 16u, 17u, 18u, 19u, 22u, 35u, 36u, 37u}) { + for (const auto unsupported : {15u, 16u, 17u, 18u, 19u, 22u}) { if (tags.contains(unsupported)) fail("Unsupported dynamic tag " + std::to_string(unsupported)); } if (tags.contains(30) && (tags.at(30) & ~(2ull | 8ull)) != 0) fail("Unsupported guest dynamic flags"); @@ -233,6 +234,17 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector && relaOffset % 24 != pltOffset % 24) fail("Unaligned overlapping relocation tables"); std::map<std::uint64_t, std::uint64_t> relocationTargets; + std::vector<Domain::DynamicTag> relrTags; + for (const auto& [tag, value] : tags) + if (tag >= 35 && tag <= 37) relrTags.push_back({static_cast<std::int64_t>(tag), value}); + for (const auto& [target, addend] : ReadRelrRelocations(bytes, image.Headers, relrTags)) { + mapped(target, 8, 2); + mapped(addend, 0, 0); + relocationTargets.emplace(target, target + 8); + Io::AppendU64(image.Dynamic.RelaData, target); + Io::AppendU64(image.Dynamic.RelaData, 8); + Io::AppendU64(image.Dynamic.RelaData, addend); + } const auto copyRelocations = [&](std::uint64_t offset, std::uint64_t size, std::vector<std::uint8_t>& output, bool plt) { output.reserve(size); for (std::uint64_t index = 0; index < size; index += 24) { diff --git a/core/relinker/relinker/src/parsing/RelrRelocations.cpp b/core/relinker/relinker/src/parsing/RelrRelocations.cpp new file mode 100644 index 0000000000..d739f4304d --- /dev/null +++ b/core/relinker/relinker/src/parsing/RelrRelocations.cpp @@ -0,0 +1,94 @@ +#include <relinker/parsing/RelrRelocations.hpp> +#include <elfpatcher/general/ElfConstants.hpp> +#include <iterator> +#include <limits> +#include <optional> + +namespace Relinker { + +std::map<Domain::VirtualAddress, std::uint64_t> ReadRelrRelocations( + const std::span<const std::uint8_t> bytes, + const std::span<const Domain::ProgramHeader> headers, + const std::span<const Domain::DynamicTag> tags) { + using namespace Elfpatcher; + const auto value = [&](const std::int64_t kind, const char* name) { + std::optional<std::uint64_t> result; + for (const auto& tag : tags) { + if (tag.Tag != kind) continue; + if (result && *result != tag.Value) + throw Domain::RelinkerException(std::string("Conflicting duplicate ") + name + " tags"); + result = tag.Value; + } + return result; + }; + const auto address = value(DT_RELR, "DT_RELR"); + const auto size = value(DT_RELRSZ, "DT_RELRSZ"); + const auto stride = value(DT_RELRENT, "DT_RELRENT"); + if (!address && !size && !stride) return {}; + if (!address || !size || !stride) + throw Domain::RelinkerException("Incomplete DT_RELR metadata"); + if (*stride != 8) + throw Domain::RelinkerException("Invalid DT_RELRENT: expected 8 bytes"); + if (*size % 8 != 0) + throw Domain::RelinkerException("Invalid DT_RELRSZ: expected a multiple of 8 bytes"); + if (*size == 0) return {}; + + for (const auto& header : headers) { + if (header.Type != PT_LOAD) continue; + if (header.FileSize > header.MemorySize || header.Offset > bytes.size() || header.FileSize > bytes.size() - header.Offset + || header.MemorySize > std::numeric_limits<std::uint64_t>::max() - header.MappedAddress) + throw Domain::RelinkerException("Invalid RELR load segment", header.Offset); + } + const auto loadAt = [&](const std::uint64_t target, const std::uint64_t extent, const bool file) -> const Domain::ProgramHeader& { + const Domain::ProgramHeader* found = nullptr; + for (const auto& header : headers) { + const auto limit = file ? header.FileSize : header.MemorySize; + if (header.Type != PT_LOAD || target < header.MappedAddress || target - header.MappedAddress > limit + || extent > limit - (target - header.MappedAddress)) continue; + if (found) throw Domain::RelinkerException("Ambiguous RELR load mapping", target); + found = &header; + } + if (!found) throw Domain::RelinkerException(file ? "RELR table exceeds file-backed load segment" : "RELR target exceeds load segment", target); + return *found; + }; + const auto& table = loadAt(*address, *size, true); + const auto tableOffset = table.Offset + (*address - table.MappedAddress); + const auto readWord = [&](const std::uint64_t offset) { + std::uint64_t result = 0; + for (std::size_t index = 0; index < 8; ++index) + result |= static_cast<std::uint64_t>(bytes[offset + index]) << (index * 8); + return result; + }; + + std::map<Domain::VirtualAddress, std::uint64_t> result; + const auto append = [&](const std::uint64_t target) { + const auto& load = loadAt(target, 8, false); + const auto next = result.lower_bound(target); + if ((next != result.end() && next->first - target < 8) + || (next != result.begin() && target - std::prev(next)->first < 8)) + throw Domain::RelinkerException("Overlapping RELR targets", target); + const auto relative = target - load.MappedAddress; + std::uint64_t addend = 0; + for (std::size_t index = 0; index < 8 && relative + index < load.FileSize; ++index) + addend |= static_cast<std::uint64_t>(bytes[load.Offset + relative + index]) << (index * 8); + result.emplace_hint(next, target, addend); + }; + std::optional<std::uint64_t> cursor; + for (std::uint64_t offset = 0; offset < *size; offset += 8) { + const auto entry = readWord(tableOffset + offset); + if ((entry & 1) == 0) { + append(entry); + cursor = entry + 8; + } else { + if (!cursor) throw Domain::RelinkerException("RELR bitmap precedes address entry", *address + offset); + if (*cursor > std::numeric_limits<std::uint64_t>::max() - 63 * 8) + throw Domain::RelinkerException("RELR bitmap address overflow", *cursor); + for (std::size_t bit = 1; bit < 64; ++bit) + if ((entry >> bit) & 1) append(*cursor + (bit - 1) * 8); + *cursor += 63 * 8; + } + } + return result; +} + +} diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index ceda75412d..fa4045ccd5 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -4,7 +4,9 @@ #include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp> #include <relinker/analysis/SceCodeExtent.hpp> #include <relinker/analysis/RelativeJumpTables.hpp> +#include <relinker/parsing/RelrRelocations.hpp> #include <algorithm> +#include <iterator> #include <sstream> #include <cstring> @@ -231,8 +233,6 @@ RelinkResult RelinkerPipeline::Relink() { if (hasTag(DT_REL) || hasTag(DT_RELSZ) || hasTag(DT_RELENT)) throw RelinkerException("DT_REL relocations are not supported"); - if (hasTag(DT_RELR) || hasTag(DT_RELRSZ) || hasTag(DT_RELRENT)) - throw RelinkerException("DT_RELR relocations are not supported"); const bool hasRelaTable = hasTag(DT_OS_RELA) || hasTag(DT_RELA) || hasTag(DT_OS_RELASZ) || hasTag(DT_RELASZ); @@ -260,6 +260,7 @@ RelinkResult RelinkerPipeline::Relink() { std::vector<std::string> neededLibraries; const std::vector<std::uint8_t>& raw = elfReader->GetRawBytes(); + const auto relrRelocations = ReadRelrRelocations(raw, programHeaders, dynTags); const std::optional<ByteCount> dynSymTabSize = hasTag(DT_OS_SYMTABSZ) ? std::optional<ByteCount>(getTagValue(DT_OS_SYMTABSZ)) : std::nullopt; @@ -373,6 +374,13 @@ RelinkResult RelinkerPipeline::Relink() { const std::uint32_t symIdx = static_cast<std::uint32_t>(rInfo >> 32); const std::uint32_t relType = static_cast<std::uint32_t>(rInfo & 0xffffffff); + if (relType != R_X86_64_NONE && !relrRelocations.empty()) { + const auto nextRelr = relrRelocations.lower_bound(rOffset); + if ((nextRelr != relrRelocations.end() && nextRelr->first - rOffset < 8) + || (nextRelr != relrRelocations.begin() && rOffset - std::prev(nextRelr)->first < 8)) + throw RelinkerException("RELR target overlaps RELA relocation", rOffset); + } + validationPolicy->ValidateRelocationTypeSupported(relType, pos); if (relType == R_X86_64_NONE) @@ -616,6 +624,8 @@ RelinkResult RelinkerPipeline::Relink() { extractLocal(dynRelaOffset, dynRelaSize, false); extractLocal(dynJmpRelOffset, dynJmpRelSize, true); + for (const auto& [target, addend] : relrRelocations) + appendRela(dynSection.RelaData, target, R_X86_64_RELATIVE, static_cast<std::int64_t>(addend)); std::vector<CallRegistryEntry> entries; entries.reserve(nidRefs.size()); diff --git a/core/relinker/relinker/tests/RelrRelocationTests.cpp b/core/relinker/relinker/tests/RelrRelocationTests.cpp new file mode 100644 index 0000000000..993b71e4a1 --- /dev/null +++ b/core/relinker/relinker/tests/RelrRelocationTests.cpp @@ -0,0 +1,123 @@ +#include <relinker/parsing/RelrRelocations.hpp> +#include <relinker/analysis/UnusedNidFilter/IRelativeRelocationIndex.hpp> +#include <io/BufferUtils.hpp> +#include <functional> +#include <iostream> +#include <limits> +#include <stdexcept> +#include <string_view> +#include <vector> + +namespace { + +void Require(const bool condition) { + if (!condition) throw std::runtime_error("RELR validation failed"); +} + +struct Fixture { + std::vector<std::uint8_t> Bytes = std::vector<std::uint8_t>(0x2000); + std::vector<Domain::ProgramHeader> Headers{{1, 7, 0, 0, 0, 0x2000, 0x3000, 0x1000}}; + std::vector<Domain::DynamicTag> Tags{{36, 0x1000}, {35, 8}, {37, 8}}; + + Fixture() { + Bytes[0] = 0x7f; Bytes[1] = 'E'; Bytes[2] = 'L'; Bytes[3] = 'F'; + Bytes[4] = 2; Bytes[5] = 1; Bytes[6] = 1; + Io::WriteU16(Bytes, 16, 3); Io::WriteU16(Bytes, 18, 62); Io::WriteU32(Bytes, 20, 1); + Io::WriteU64(Bytes, 32, 64); Io::WriteU16(Bytes, 52, 64); + Io::WriteU16(Bytes, 54, 56); Io::WriteU16(Bytes, 56, 2); + Io::WriteU32(Bytes, 64, 1); Io::WriteU32(Bytes, 68, 7); + Io::WriteU64(Bytes, 96, 0x2000); Io::WriteU64(Bytes, 104, 0x3000); Io::WriteU64(Bytes, 112, 0x1000); + Io::WriteU32(Bytes, 120, 2); Io::WriteU64(Bytes, 128, 0x100); Io::WriteU64(Bytes, 136, 0x100); + Io::WriteU64(Bytes, 152, 64); Io::WriteU64(Bytes, 160, 64); + for (std::size_t index = 0; index < Tags.size(); ++index) { + Io::WriteU64(Bytes, 0x100 + index * 16, Tags[index].Tag); + Io::WriteU64(Bytes, 0x108 + index * 16, Tags[index].Value); + } + for (std::size_t offset = 0x200; offset < 0x900; offset += 8) Io::WriteU64(Bytes, offset, offset + 0x900); + SetWords({0x200}); + } + + void SetWords(const std::initializer_list<std::uint64_t> words) { + Tags[1].Value = words.size() * 8; + Io::WriteU64(Bytes, 0x118, words.size() * 8); + std::size_t offset = 0x1000; + for (const auto word : words) { Io::WriteU64(Bytes, offset, word); offset += 8; } + } + + auto Read() const { return Relinker::ReadRelrRelocations(Bytes, Headers, Tags); } +}; + +void Reject(const std::function<void(Fixture&)>& change, const std::string_view diagnostic) { + Fixture fixture; + change(fixture); + try { fixture.Read(); } + catch (const Domain::RelinkerException& error) { + Require(std::string_view(error.what()).find(diagnostic) != std::string_view::npos); + return; + } + throw std::runtime_error("Malformed RELR input accepted"); +} + +} + +int main() try { + Fixture fixture; + Require(fixture.Read() == std::map<Domain::VirtualAddress, std::uint64_t>{{0x200, 0xb00}}); + for (unsigned bit = 1; bit < 64; ++bit) { + fixture.SetWords({0x200, 1ull | (1ull << bit), 1ull | (1ull << 63)}); + const auto result = fixture.Read(); + const auto target = 0x208 + (bit - 1) * 8; + Require(result.size() == 3 && result.at(0x200) == 0xb00 && result.at(target) == target + 0x900); + Require(result.at(0x5f0) == 0xef0); + const auto index = Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(fixture.Bytes); + Require(index->TargetOfSlot(target) == target + 0x900 && index->TargetOfSlot(0x5f0) == 0xef0); + } + fixture.SetWords({0x200, std::numeric_limits<std::uint64_t>::max()}); + Require(fixture.Read().size() == 64); + fixture.SetWords({0x200, 1, 3}); + Require(fixture.Read().size() == 2 && fixture.Read().at(0x400) == 0xd00); + fixture.SetWords({0x800, 0x202}); + Require(fixture.Read().size() == 2 && fixture.Read().at(0x202) == 0x0b08000000000000ull); + fixture.SetWords({0x2000}); + Require(fixture.Read().at(0x2000) == 0); + fixture.SetWords({0x1ffc}); + Io::WriteU32(fixture.Bytes, 0x1ffc, 0x12345678); + Require(fixture.Read().at(0x1ffc) == 0x12345678); + fixture.SetWords({}); + Require(fixture.Read().empty()); + fixture.Tags.clear(); + Require(fixture.Read().empty()); + + for (unsigned mask = 1; mask < 7; ++mask) { + Reject([&](auto& input) { + const auto tags = input.Tags; + input.Tags.clear(); + for (unsigned index = 0; index < 3; ++index) if (mask & (1u << index)) input.Tags.push_back(tags[index]); + }, "Incomplete DT_RELR metadata"); + } + Reject([](auto& input) { input.Tags[2].Value = 4; }, "DT_RELRENT"); + Reject([](auto& input) { input.Tags[1].Value = 9; }, "DT_RELRSZ"); + for (std::size_t index = 0; index < 3; ++index) { + Reject([&](auto& input) { auto tag = input.Tags[index]; ++tag.Value; input.Tags.push_back(tag); }, "Conflicting duplicate"); + } + Reject([](auto& input) { input.SetWords({3}); }, "bitmap precedes address"); + Reject([](auto& input) { input.SetWords({0x200, 0x200}); }, "Overlapping RELR targets"); + Reject([](auto& input) { input.SetWords({0x200, 0x206}); }, "Overlapping RELR targets"); + Reject([](auto& input) { input.SetWords({0x3000}); }, "target exceeds load"); + Reject([](auto& input) { input.SetWords({0x2ffc}); }, "target exceeds load"); + Reject([](auto& input) { input.Tags[0].Value = 0x2000; }, "table exceeds file-backed"); + Reject([](auto& input) { input.Tags[0].Value = 0x1ffc; }, "table exceeds file-backed"); + Reject([](auto& input) { input.Tags[1].Value = 0xfffffffffffffff8ull; }, "table exceeds file-backed"); + Reject([](auto& input) { input.Headers[0].Offset = 0xffffffffffffffffull; }, "Invalid RELR load"); + Reject([](auto& input) { input.Headers[0].MemorySize = 0x1000; }, "Invalid RELR load"); + Reject([](auto& input) { input.Headers.push_back(input.Headers.front()); }, "Ambiguous RELR load"); + Reject([](auto& input) { + const auto address = std::numeric_limits<std::uint64_t>::max() - 0x3f; + input.Headers.push_back({1, 6, 0, address, 0, 0, 0x38, 1}); + input.SetWords({address, 1}); + }, "bitmap address overflow"); + std::cout << "RELR address, bitmap, bounds, addend and reachability tests passed\n"; +} catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; +} diff --git a/core/relinker/relinker/tests/test_optional_rela.py b/core/relinker/relinker/tests/test_optional_rela.py index 45b16fe10d..8efac0ff25 100644 --- a/core/relinker/relinker/tests/test_optional_rela.py +++ b/core/relinker/relinker/tests/test_optional_rela.py @@ -123,8 +123,10 @@ def main(): other = tag if sce else SCE_TAGS[tag] invalid += ((f"ambiguous-{tag}", ((7, 0x700), (8, 0), (9, 24)), ((other, value),), "Both DT_OS_ and DT_ variants"),) - for tag in (17, 18, 19, 35, 36, 37): + for tag in (17, 18, 19): invalid += ((f"unsupported-{tag}", (), ((tag, 24),), "relocations are not supported"),) + for tag in (35, 36, 37): + invalid += ((f"incomplete-relr-{tag}", (), ((tag, 24),), "Incomplete DT_RELR metadata"),) for name, tags, extra, error in invalid: source = work / f"{sce}-{name}.elf" source.write_bytes(fixture(sce, tags, extra_tags=extra)) diff --git a/core/relinker/relinker/tests/test_relr_relocations.py b/core/relinker/relinker/tests/test_relr_relocations.py new file mode 100644 index 0000000000..9c79447db6 --- /dev/null +++ b/core/relinker/relinker/tests/test_relr_relocations.py @@ -0,0 +1,179 @@ +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from guest_import_fixtures import dynamic, executable +from test_guest_symbol_names import dynamic_tags, elf_loads, file_offset +from test_optional_plt import fixture +from test_windows_import_modules import provider + + +SYSTEMS = (("linux", [], b"\x7fELF"), + ("linux-pie", ["--lazy-binding"], b"\x7fELF"), + ("windows", ["--windows"], b"MZ"), + ("macos", ["--macos"], b"\xcf\xfa\xed\xfe")) + + +def main_image(words=(0x800,), mixed=False): + image = fixture() + image[0x200:0x206] = b"\xff\x25" + struct.pack("<i", 0x800 - 0x206) + struct.pack_into("<Q", image, 64 + 40, 0x2000) + tags = [(5, 0x600), (10, 1), (6, 0x620), (11, 24), + (36, 0x700), (35, len(words) * 8), (37, 8)] + if mixed: + tags.extend(((7, 0x740), (8, 24), (9, 24))) + struct.pack_into("<QQq", image, 0x740, 0x850, 8, 0x210) + dynamic(image, 0x400, 120, tags) + for index, word in enumerate(words): + struct.pack_into("<Q", image, 0x700 + index * 8, word) + for target in (0x800, 0x808, 0x9f8, 0xa00): + struct.pack_into("<Q", image, target, 0x210) + return image + + +def guest_image(words=(0x2320, 5), mixed=False): + code = bytearray(b"\x90" * 0x4b) + code[:6] = b"\xff\x25" + struct.pack("<i", 0x2320 - 0x1006) + code[0x20:0x2a] = b"\x8b\x05" + struct.pack("<i", 0x2340 - 0x1026) + b"\x83\xc0\x04\xc3" + code[0x40:0x4b] = b"\xc7\x05" + struct.pack("<i", 0x2340 - 0x104a) + struct.pack("<I", 7) + b"\xc3" + image = provider(11, code=code) + tags = [(kind, value) for kind, value in dynamic_tags(image).items() if kind not in (7, 8, 9)] + tags.extend(((36, 0x2380), (35, len(words) * 8), (37, 8), (25, 0x2330), (27, 8))) + if mixed: + tags.extend(((7, 0x2300), (8, 24), (9, 24))) + struct.pack_into("<QQq", image, 0x900, 0x2350, 8, 0x1020) + dynamic(image, 0x600, 176, tags) + for index, word in enumerate(words): + struct.pack_into("<Q", image, 0x980 + index * 8, word) + struct.pack_into("<Q", image, 0x920, 0x1020) + struct.pack_into("<Q", image, 0x930, 0x1040) + aligned = bytearray(0x3000) + aligned[:0x400] = image[:0x400] + aligned[0x1000:0x1100] = image[0x400:0x500] + aligned[0x2000:0x2400] = image[0x600:0xa00] + for header, offset in ((64, 0x1000), (120, 0x2000), (176, 0x2000)): + struct.pack_into("<Q", aligned, header + 8, offset) + for header in (64, 120): + struct.pack_into("<Q", aligned, header + 48, 0x1000) + return aligned + + +def replace_tag(image, kind, value, guest): + start = 0x2000 if guest else 0x400 + for offset in range(start, start + 0x200, 16): + tag = struct.unpack_from("<q", image, offset)[0] + if tag == kind: + struct.pack_into("<Q", image, offset + 8, value) + return image + if tag == 0: + break + raise AssertionError(kind) + + +def linux_relocations(path): + image = path.read_bytes() + tags = dynamic_tags(image) + assert not {35, 36, 37} & tags.keys(), tags + if tags.get(8, 0) == 0: + return [] + offset = file_offset(image, tags[7]) + return [struct.unpack_from("<QQQ", image, position) for position in range(offset, offset + tags[8], 24)] + + +def main(): + relinker = Path(sys.argv[1]).resolve() + guest = len(sys.argv) > 2 and sys.argv[2] == "guest" + build = guest_image if guest else main_image + anchor = 0x2320 if guest else 0x800 + positive = (("addresses", (0x2320, 0x2330) if guest else (anchor,), False), + ("bitmap", (anchor, 5) if guest else (anchor, 3 | (1 << 63), 3), False), + ("mixed", (anchor, 5) if guest else (anchor, 3), True)) + with tempfile.TemporaryDirectory(prefix="anyps5-relr-") as directory: + work = Path(directory) + + def convert(label, image, error=None, expected=None): + for system, options, magic in SYSTEMS: + if guest and system == "linux-pie": + continue + case = work / label / system + case.mkdir(parents=True) + source = case / "input.elf" + if guest: + modules = case / "sce_module" + modules.mkdir() + module = modules / "provider.prx" + module.write_bytes(image) + source.write_bytes(executable("provider.prx")) + else: + source.write_bytes(image) + output = case / ("output.exe" if system == "windows" else "output.bin") + result = subprocess.run([str(relinker), *options, *([] if guest else ["--skip-sce-module"]), + str(source), str(output)], capture_output=True, text=True, timeout=10) + if error: + assert result.returncode == 2 and error in result.stderr and not output.exists(), (label, system, result.stderr) + continue + assert result.returncode == 0 and output.read_bytes().startswith(magic), (label, system, result.stderr) + if guest: + assert module.read_bytes() == image + if system.startswith("linux") and expected is not None: + artifact = case / "app0/sce_module/provider.prx.guest.prx" if guest else output + assert all(header[2] % 0x1000 == header[3] % 0x1000 for header in elf_loads(artifact.read_bytes())) + observed = linux_relocations(artifact) + wanted = {(target + 0x400000, kind, addend + 0x400000) for target, kind, addend in expected} if system == "linux" and not guest else expected + assert set(observed) == wanted and len(observed) == len(wanted), (label, observed, wanted) + if (system == "windows" and os.name == "nt") or (system.startswith("linux") and sys.platform == "linux"): + executed = subprocess.run([str(output)], capture_output=True, text=True, timeout=10) + assert executed.returncode == (11 if guest else 42), (label, system, executed.returncode, executed.stderr) + + for label, words, mixed in positive: + if guest: + expected = {(0x2320, 8, 0x1020), (0x2330, 8, 0x1040)} + if mixed: + expected.add((0x2350, 8, 0x1020)) + else: + targets = {0x800} + if label == "bitmap": + targets.update((0x808, 0x9f8, 0xa00)) + if mixed: + targets.update((0x808, 0x850)) + expected = {(target, 8, 0x210) for target in targets} + convert(label, build(words, mixed), expected=expected) + + if not guest: + bss = main_image((0x800, 0x1008, 0xffc)) + struct.pack_into("<I", bss, 0xffc, 0x210) + convert("zero-fill-addends", bss, expected={(0x800, 8, 0x210), (0x1008, 8, 0), (0xffc, 8, 0x210)}) + unaligned = main_image((0x802,)) + unaligned[0x200:0x206] = b"\xff\x25" + struct.pack("<i", 0x802 - 0x206) + struct.pack_into("<Q", unaligned, 0x802, 0x210) + convert("even-unaligned-slot", unaligned, expected={(0x802, 8, 0x210)}) + empty = main_image(()) + empty[0x200:0x206] = b"\xb8\x2a\x00\x00\x00\xc3" + convert("empty", empty, expected=set()) + + convert("bitmap-first", build((3,)), "RELR bitmap precedes address entry") + convert("overlap", build((anchor, anchor)), "Overlapping RELR targets") + convert("partial-overlap", build((anchor, anchor + 2)), "Overlapping RELR targets") + convert("entry-size", replace_tag(build(), 37, 4, guest), "Invalid DT_RELRENT") + convert("table-size", replace_tag(build(), 35, 9, guest), "Invalid DT_RELRSZ") + convert("table-range", replace_tag(build(), 36, 0xfffffffffffffff8, guest), "RELR table exceeds file-backed") + convert("size-overflow", replace_tag(build(), 35, 0xfffffffffffffff8, guest), "RELR table exceeds file-backed") + convert("target-range", build((0xfffffffffffffffe,)), "RELR target exceeds load") + incomplete = build() + start = 0x2000 if guest else 0x400 + for offset in range(start, start + 0x200, 16): + if struct.unpack_from("<q", incomplete, offset)[0] == 37: + struct.pack_into("<q", incomplete, offset, 0x60000001) + break + convert("incomplete", incomplete, "Incomplete DT_RELR metadata") + overlap = build(mixed=True) + struct.pack_into("<Q", overlap, 0x2300 if guest else 0x740, anchor) + convert("relr-rela-overlap", overlap, "Overlapping guest relocations" if guest else "RELR target overlaps RELA relocation") + print("Guest RELR conversion and initializer tests passed" if guest else "Executable RELR conversion tests passed") + + +if __name__ == "__main__": + main() diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index e4049858c1..76f8999911 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -2,6 +2,8 @@ ### Functional +- [Packed relative relocations](../../../core/relinker/relinker/src/parsing/RelrRelocations.cpp) require the table to fit in one file-backed PT_LOAD segment and each 8-byte storage unit to fit in one load segment. Units spanning load segments, overlapping RELR units and overlaps with RELA relocations are rejected; relocation composition is not implemented. + - The [syscall scanner](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) decodes executable ranges linearly without code/data boundaries. Embedded data between routines can fail decoding or be reported as a forbidden syscall ([#7547](https://github.com/boykopovar/AnyPS5/issues/7547)). Unwind ranges alone omit real code. The [synthetic fixture](../../../core/relinker/relinker/tests/CodeDataScanningTests.cpp) characterizes these rejections and checks a reachable routine beyond the gap; it does not resolve the boundary problem. - [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through `--module-dir` without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured. From 1c02c55cef46187e6401001440bd12e4b99a3b39 Mon Sep 17 00:00:00 2001 From: Matheus Herberts <163153928+Mherberts@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:03:36 -0300 Subject: [PATCH 543/702] test(relinker): validate Linux fixture results before trapping --- .../relinker/tests/test_relr_relocations.py | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/core/relinker/relinker/tests/test_relr_relocations.py b/core/relinker/relinker/tests/test_relr_relocations.py index 9c79447db6..78828872e0 100644 --- a/core/relinker/relinker/tests/test_relr_relocations.py +++ b/core/relinker/relinker/tests/test_relr_relocations.py @@ -1,11 +1,14 @@ import os from pathlib import Path +import platform +import signal import struct import subprocess import sys import tempfile from guest_import_fixtures import dynamic, executable +from signal_run import run_without_core_dump from test_guest_symbol_names import dynamic_tags, elf_loads, file_offset from test_optional_plt import fixture from test_windows_import_modules import provider @@ -83,6 +86,19 @@ def linux_relocations(path): return [struct.unpack_from("<QQQ", image, position) for position in range(offset, offset + tags[8], 24)] +def linux_checked_entry(image, expected): + image = bytearray(image) + entry = struct.unpack_from("<Q", image, 24)[0] + checked_entry = entry + 0x40 + code = b"\xe8" + struct.pack("<i", entry - checked_entry - 5) + code += b"\x3d" + struct.pack("<I", expected) + b"\x74\x02\x0f\x0b\xcc" + offset = file_offset(image, checked_entry) + assert offset + len(code) <= len(image) + image[offset:offset + len(code)] = code + struct.pack_into("<Q", image, 24, checked_entry) + return image + + def main(): relinker = Path(sys.argv[1]).resolve() guest = len(sys.argv) > 2 and sys.argv[2] == "guest" @@ -106,9 +122,12 @@ def main(): modules.mkdir() module = modules / "provider.prx" module.write_bytes(image) - source.write_bytes(executable("provider.prx")) + input_image = executable("provider.prx") else: - source.write_bytes(image) + input_image = image + if system.startswith("linux"): + input_image = linux_checked_entry(input_image, 11 if guest else 42) + source.write_bytes(input_image) output = case / ("output.exe" if system == "windows" else "output.bin") result = subprocess.run([str(relinker), *options, *([] if guest else ["--skip-sce-module"]), str(source), str(output)], capture_output=True, text=True, timeout=10) @@ -124,9 +143,13 @@ def main(): observed = linux_relocations(artifact) wanted = {(target + 0x400000, kind, addend + 0x400000) for target, kind, addend in expected} if system == "linux" and not guest else expected assert set(observed) == wanted and len(observed) == len(wanted), (label, observed, wanted) - if (system == "windows" and os.name == "nt") or (system.startswith("linux") and sys.platform == "linux"): + if system == "windows" and os.name == "nt": executed = subprocess.run([str(output)], capture_output=True, text=True, timeout=10) assert executed.returncode == (11 if guest else 42), (label, system, executed.returncode, executed.stderr) + if system.startswith("linux") and sys.platform == "linux" and platform.machine() in ("x86_64", "AMD64"): + output.chmod(0o755) + executed = run_without_core_dump([str(output)], capture_output=True, text=True, timeout=10) + assert executed.returncode == -signal.SIGTRAP, (label, system, executed.returncode, executed.stderr) for label, words, mixed in positive: if guest: From 06a1dec6fb2ed7d9d827c9ed21a62402040dcbc1 Mon Sep 17 00:00:00 2001 From: Matheus Herberts <163153928+Mherberts@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:06:00 -0300 Subject: [PATCH 544/702] chore(relinker): separate RELR include from data-table changes --- core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index fa4045ccd5..81e1f4173e 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -4,7 +4,6 @@ #include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp> #include <relinker/analysis/SceCodeExtent.hpp> #include <relinker/analysis/RelativeJumpTables.hpp> -#include <relinker/parsing/RelrRelocations.hpp> #include <algorithm> #include <iterator> #include <sstream> @@ -14,6 +13,7 @@ #include <tuple> #include <utility> #include <domain/ImportModule.hpp> +#include <relinker/parsing/RelrRelocations.hpp> namespace Relinker { From 7c90e64474ec7d3ebc6d143f06f6e2ad3b04a726 Mon Sep 17 00:00:00 2001 From: Matheus Herberts <163153928+Mherberts@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:59:03 -0300 Subject: [PATCH 545/702] chore(relinker): group RELR tests with relocation tests --- core/relinker/CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 451ef4eade..e6a87dd599 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -566,6 +566,11 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $<TARGET_FILE:relinker>) add_test(NAME optional_rela COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_rela.py $<TARGET_FILE:relinker>) + add_test(NAME relr_conversion COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_relr_relocations.py $<TARGET_FILE:relinker>) + add_test(NAME guest_relr_conversion COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_relr_relocations.py $<TARGET_FILE:relinker> guest) + set_tests_properties(relr_conversion guest_relr_conversion PROPERTIES TIMEOUT 20) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $<TARGET_FILE:relinker>) add_test(NAME windows_tls_zero_fill COMMAND ${Python3_EXECUTABLE} @@ -812,11 +817,6 @@ if(BUILD_TESTING) endif() add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $<TARGET_FILE:relinker>) - add_test(NAME relr_conversion COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_relr_relocations.py $<TARGET_FILE:relinker>) - add_test(NAME guest_relr_conversion COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_relr_relocations.py $<TARGET_FILE:relinker> guest) - set_tests_properties(relr_conversion guest_relr_conversion PROPERTIES TIMEOUT 20) add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_weak_imports.py $<TARGET_FILE:relinker>) add_test(NAME windows_guest_weak_imports COMMAND ${Python3_EXECUTABLE} From ff5da447b4ec5496437d03ef9fa70d5213263e03 Mon Sep 17 00:00:00 2001 From: slopgobblercobbler <slopgobblercobbler@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:53:51 +0100 Subject: [PATCH 546/702] fix(libSceUserService): validate user in sceUserServiceGetPlatformPrivacyWs1 --- core/libs/prx/libSceUserService/Export.cpp | 11 +++++++---- core/libs/tests/GuestUserService.cpp | 20 ++++++++++++++++++++ docs/dev/debt/libSceUserService.md | 1 + 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index fa8edb8b62..07a31f1f56 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -296,12 +296,15 @@ int APS5_VABI sceUserServiceTerminate(void) { return USER_SERVICE_OK; } -// No PSN account exists, so the platform privacy setting reports the feature as not permitted. int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* value) { - (void)user_id; - if (!value) return USER_SERVICE_ERROR_INVALID_ARGUMENT; + if (!IsUserLoggedIn(user_id)) { + return USER_SERVICE_ERROR_NOT_LOGGED_IN; + } + if (value == nullptr) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } *value = 0; - return 0; + return USER_SERVICE_OK; } int APS5_VABI sceUserServiceGetForegroundUser(int* user_id) { diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 9b46dffc13..9b8ce9d612 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -33,6 +33,10 @@ int main() { Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID, &privacy) == USER_SERVICE_OK); Require(privacy == 0); + privacy = -1; + Require(sceUserServiceGetPlatformPrivacyWs1(123, &privacy) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(privacy == -1); + Require(sceUserServiceGetPlatformPrivacyWs1(123, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); int foreground = -1; Require(sceUserServiceGetForegroundUser(&foreground) == USER_SERVICE_OK); @@ -107,6 +111,10 @@ int main() { 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); + privacy = 0x5a5a; + Require(sceUserServiceGetPlatformPrivacyWs1(user_id, &privacy) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(privacy == 0x5a5a); + Require(sceUserServiceGetPlatformPrivacyWs1(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); @@ -181,6 +189,10 @@ int main() { Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, &age_level) == USER_SERVICE_ERROR_NOT_LOGGED_IN); Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + privacy = 0x5a5a; + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID + 1, &privacy) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(privacy == 0x5a5a); + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); for (const Setting& setting : settings) { std::int32_t value = 0x5a5a5a5a; Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, &value) == USER_SERVICE_ERROR_NOT_LOGGED_IN); @@ -197,6 +209,10 @@ int main() { } Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, &age_level) == USER_SERVICE_OK); Require(age_level == 0xffffffffu); + privacy = 0x5a5a; + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID + 1, &privacy) == USER_SERVICE_OK); + Require(privacy == 0); for (const Setting& setting : settings) { std::int32_t value = 0x5a5a5a5a; Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, &value) == USER_SERVICE_OK); @@ -211,6 +227,10 @@ int main() { } Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, &age_level) == USER_SERVICE_ERROR_NOT_LOGGED_IN); Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + privacy = 0x5a5a; + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID + 1, &privacy) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(privacy == 0x5a5a); + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); for (const Setting& setting : settings) { std::int32_t value = 0x5a5a5a5a; Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, &value) == USER_SERVICE_ERROR_NOT_LOGGED_IN); diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index ad2cdd7242..d694250136 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -10,3 +10,4 @@ - [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. The one call in PPSA12544 (0x16a80a0), made only when bit 0x4 of its flags is set, writes the value to a field of its own object (offset 0x5170) and ends its chain of getters on any nonzero result; what it does with the value afterwards was not read. - [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. One title (PPSA04354) disassembled for this entry calls all six in a row (first at 0xea0efa) with its user ID, only when a flag bit (0x10) is set; a result other than 0 from any of them ends the block and is returned, and only when all six return 0 does it store the six values in its object. - [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's (languages 0 to 29); a valid language outside it, such as Ukrainian (30), reports 20 (US English). A value that is not a console language number is rejected by `sceSystemServiceParamGetInt` with an error naming `ANYPS5_LANGUAGE`; the call throws it and leaves the output untouched. The input variant is 0 because the owner's table gives 0 for Japanese and Traditional Chinese, the only languages needing one. `sceConvertKeycodeGetImeKeyboardType` consumes these values. +- [sceUserServiceGetPlatformPrivacyWs1](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the platform privacy setting for an offline user without a linked PSN account is assumed to be 0 (not permitted) with success; actual console privacy querying is not modelled. Return of `0x80960009` (`USER_SERVICE_ERROR_NOT_LOGGED_IN`) with or without an output for a user who is not logged in is borrowed from `sceUserServiceGetAgeLevel` and is not measured for this call. From ac159447ea4af11e66226e3ef39525e0bc6f0f1a Mon Sep 17 00:00:00 2001 From: SAKIBUL HUDA ONNAY <onnoy29@gmail.com> Date: Tue, 27 Oct 2026 22:04:46 +0600 Subject: [PATCH 547/702] test(libkernel): remove the symlink fixture so cleanup does not hang on Windows --- core/libs/tests/GuestFileTimestamps.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 8a081181a5..21eb2b2575 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -300,6 +300,9 @@ int main() { Require(RemoveDirectoryW(std::filesystem::path(directoryLink).c_str()) && DeleteFileW(std::filesystem::path(fileLink).c_str()) && DeleteFileW(std::filesystem::path(danglingLink).c_str()), "remove the link fixtures"); } +#endif +#ifdef _WIN32 + DeleteFileW(std::filesystem::path(linkPath).c_str()); #endif std::filesystem::remove_all(root); } From 488bd76652bc132e5ae3bacb634a84a96d64e07b Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:45:18 +0530 Subject: [PATCH 548/702] fix(tools): handle missing git executable in check_main_sync --- tools/check_main_sync.py | 2 +- tools/tests/test_check_main_sync.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index f18823a925..b8f2c1fa21 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -129,7 +129,7 @@ def main(argv=None): break base = resolve_base(args.repo, args.base) data = report(args.repo, base, args.head) - except RuntimeError as error: + except (RuntimeError, OSError) as error: parser.error(str(error)) print(render(data)) if data["conflicts"] or (args.strict and data["behind"]): diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index 5319dbbfc7..4f55f0ad19 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -299,6 +299,18 @@ class CheckMainSyncTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("merge: clean", result.stdout) + def test_missing_git_executable_reports_usage_error_without_traceback(self): + def missing_run(*args, **kwargs): + raise FileNotFoundError(2, "No such file or directory: 'git'") + + stdout, stderr = io.StringIO(), io.StringIO() + with unittest.mock.patch("subprocess.run", side_effect=missing_run), contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + with self.assertRaises(SystemExit) as raised: + check_main_sync.main(["--repo", str(self.repo.path)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn("No such file or directory: 'git'", stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + if __name__ == "__main__": unittest.main() From a06fb13f6c9b0199ec88c9da2256714bbec6ca1c Mon Sep 17 00:00:00 2001 From: simnJS <simnkrystal@gmail.com> Date: Tue, 27 Oct 2026 17:23:33 +0100 Subject: [PATCH 549/702] fix(libSceAutoMounterClient): name the fields of the USB device-info output The 816 bytes after the 128-byte device name are the PS4 SceAutoMounterClientUsbDeviceInfo fields up to the serial length in flatz's ps4_stub_lib_maker_v2 header: the total and available space, the vendor id, product id and release, and the manufacturer, product and serial strings with their lengths. libSceUsbStorage's device-info record starts with the same layout. Every field offset is now asserted; the size and offsets do not change. --- .../prx/libSceAutoMounterClient/Export.cpp | 17 ++++++++++------ .../libSceAutoMounterClient/include/Api.hpp | 20 +++++++++++-------- docs/dev/debt/libSceAutoMounterClient.md | 2 +- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAutoMounterClient/Export.cpp b/core/libs/prx/libSceAutoMounterClient/Export.cpp index ab7eed9165..63b11a74c2 100644 --- a/core/libs/prx/libSceAutoMounterClient/Export.cpp +++ b/core/libs/prx/libSceAutoMounterClient/Export.cpp @@ -5,12 +5,17 @@ #include "prx/libc/include/General.hpp" static_assert(sizeof(AutoMounterUsbDeviceInfo) == 944); -static_assert(offsetof(AutoMounterUsbDeviceInfo, data0) == 150); -static_assert(offsetof(AutoMounterUsbDeviceInfo, data0Size) == 408); -static_assert(offsetof(AutoMounterUsbDeviceInfo, data1) == 416); -static_assert(offsetof(AutoMounterUsbDeviceInfo, data1Size) == 672); -static_assert(offsetof(AutoMounterUsbDeviceInfo, data2) == 680); -static_assert(offsetof(AutoMounterUsbDeviceInfo, data2Size) == 936); +static_assert(offsetof(AutoMounterUsbDeviceInfo, totalSpace) == 128); +static_assert(offsetof(AutoMounterUsbDeviceInfo, availableSpace) == 136); +static_assert(offsetof(AutoMounterUsbDeviceInfo, vendorId) == 144); +static_assert(offsetof(AutoMounterUsbDeviceInfo, productId) == 146); +static_assert(offsetof(AutoMounterUsbDeviceInfo, bcdDevice) == 148); +static_assert(offsetof(AutoMounterUsbDeviceInfo, manufacturer) == 150); +static_assert(offsetof(AutoMounterUsbDeviceInfo, manufacturerLength) == 408); +static_assert(offsetof(AutoMounterUsbDeviceInfo, product) == 416); +static_assert(offsetof(AutoMounterUsbDeviceInfo, productLength) == 672); +static_assert(offsetof(AutoMounterUsbDeviceInfo, serialNumber) == 680); +static_assert(offsetof(AutoMounterUsbDeviceInfo, serialNumberLength) == 936); extern "C" { diff --git a/core/libs/prx/libSceAutoMounterClient/include/Api.hpp b/core/libs/prx/libSceAutoMounterClient/include/Api.hpp index 766f754f18..fc0dc320cc 100644 --- a/core/libs/prx/libSceAutoMounterClient/include/Api.hpp +++ b/core/libs/prx/libSceAutoMounterClient/include/Api.hpp @@ -4,14 +4,18 @@ struct AutoMounterUsbDeviceInfo { char deviceName[128]; - std::byte metadata[22]; - std::byte data0[256]; - std::byte padding[2]; - std::uint64_t data0Size; - std::byte data1[256]; - std::uint64_t data1Size; - std::byte data2[256]; - std::uint64_t data2Size; + std::uint64_t totalSpace; + std::uint64_t availableSpace; + std::uint16_t vendorId; + std::uint16_t productId; + std::uint16_t bcdDevice; + char manufacturer[256]; + std::uint8_t padding[2]; + std::uint64_t manufacturerLength; + char product[256]; + std::uint64_t productLength; + char serialNumber[256]; + std::uint64_t serialNumberLength; }; extern "C" { diff --git a/docs/dev/debt/libSceAutoMounterClient.md b/docs/dev/debt/libSceAutoMounterClient.md index e3840eb3de..b46745f559 100644 --- a/docs/dev/debt/libSceAutoMounterClient.md +++ b/docs/dev/debt/libSceAutoMounterClient.md @@ -3,5 +3,5 @@ ### Unknown function info - [sceAutoMounterClientRegisterCallback, sceAutoMounterClientTerm and sceAutoMounterClientUnregisterCallback](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp) (libSceAutoMounterClient) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAutoMounterClient.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceAutoMounterClientGetUsbDeviceInfo](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp): the 22 metadata bytes at offset 128 remain opaque. The 944-byte output layout, 128-byte device name and three 256-byte arrays with 64-bit lengths are recovered. +- [sceAutoMounterClientGetUsbDeviceInfo](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp) (libSceAutoMounterClient) - the fields after the 128-byte device name are named after flatz's PS4 [`SceAutoMounterClientUsbDeviceInfo`](https://github.com/flatz/ps4_stub_lib_maker_v2/blob/a8d044c12ea2dc3fa190ce1769eeb48b30d28ed4/include/automount.h#L11-L33): total and available space, vendor id, product id, release, and the manufacturer, product and serial strings with their 64-bit lengths. The 944-byte PS5 output holds that structure's first 816 bytes, at the offsets libSceUsbStorage's [device-info record](libSceUsbStorage.md) also uses (the vendor id at 0x10 and the strings at 0x16, 0x120 and 0x228); the PS4 fields after the serial length are not part of it. What the console writes into each field is not measured, and the function still throws. - [sceAutoMounterClientInit, sceAutoMounterClientGetUsbDeviceList](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp) (libSceAutoMounterClient) - throw through `NotImplemented_nid_no_patch`. Client initialization thread attributes and device list query interactions with the automounter service remain unimplemented. From 8e1e9585921c57eb5d32228427fce2ca57ae2c41 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Wed, 28 Oct 2026 05:25:36 +1300 Subject: [PATCH 550/702] test(libkernel): link libc into guest_amm_pool_tests for the Windows C++ runtime The test throws std::bad_alloc from a replaced operator new, so on MinGW it needs the same libc link, unwind section rename and DLL path as the other throwing guest tests (the Windows link failed on __gxx_personality_v0). --- core/libs/prx/libkernel/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2c0cc9124f..5b00e211e7 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1175,8 +1175,14 @@ if(BUILD_TESTING) endif() add_test_executable(guest_amm_pool_tests ${LIBS_INCLUDE_DIR}/tests/GuestAmmPool.cpp) target_include_directories(guest_amm_pool_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_amm_pool_tests PRIVATE libc) + configure_windows_unwind(guest_amm_pool_tests) add_test(NAME guest_amm_pool COMMAND guest_amm_pool_tests) set_tests_properties(guest_amm_pool PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_amm_pool PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test_executable(guest_raise_exception_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseException.cpp) target_include_directories(guest_raise_exception_tests PRIVATE ${LIBS_INCLUDE_DIR}) From fc2bda11b13c3bde86bd6cd5efe9a70b4f06f3be Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:56:40 +0530 Subject: [PATCH 551/702] fix(tools): validate destination command path in pad_timeline --- tools/pad_timeline.py | 4 ++++ tools/tests/test_pad_timeline.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/tools/pad_timeline.py b/tools/pad_timeline.py index 16672d228e..0d2f08eb82 100644 --- a/tools/pad_timeline.py +++ b/tools/pad_timeline.py @@ -86,6 +86,10 @@ def main(): try: if args.timeline.resolve() == args.path.resolve() or (args.path.exists() and os.path.samefile(args.timeline, args.path)): raise ValueError("the command file must be separate from the timeline JSON") + if args.path.is_dir(): + raise ValueError(f"{args.path} is a directory") + if not args.path.parent.is_dir(): + raise ValueError(f"directory does not exist: {args.path.parent}") document = json.loads(args.timeline.read_text(encoding="utf-8")) text = compile_timeline(document, next_sequence(args.path), args.repeat, args.gap_ms) queue_timeline(args.path, text) diff --git a/tools/tests/test_pad_timeline.py b/tools/tests/test_pad_timeline.py index b7a91f3d96..576ce78576 100644 --- a/tools/tests/test_pad_timeline.py +++ b/tools/tests/test_pad_timeline.py @@ -155,6 +155,21 @@ class TimelineTests(unittest.TestCase): self.assertEqual(self.run_cli(timeline, linked).returncode, 2) self.assertEqual(timeline.read_text(encoding="utf-8"), original) + def test_output_directory_validation(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline = root / "sequence.json" + timeline.write_text(json.dumps(self.document), encoding="utf-8") + target_dir = root / "output_dir" + target_dir.mkdir() + result = self.run_cli(timeline, target_dir) + self.assertEqual(result.returncode, 2) + self.assertIn(f"{target_dir} is a directory", result.stderr) + nonexistent_parent = root / "missing" / "command" + result = self.run_cli(timeline, nonexistent_parent) + self.assertEqual(result.returncode, 2) + self.assertIn(f"directory does not exist: {nonexistent_parent.parent}", result.stderr) + def test_sequence_advances_past_existing_command_after_clock_reset(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) From 4a5ac57bd36dc28f26287a38f5b8deda5fb8dcfc Mon Sep 17 00:00:00 2001 From: Disaster <Disvster.wav@gmail.com> Date: Tue, 27 Oct 2026 19:56:38 +0300 Subject: [PATCH 552/702] test(libSceFont): cover render surface scissor clipping --- core/libs/prx/libSceFont/CMakeLists.txt | 10 ++ core/libs/tests/GuestFontScissor.cpp | 225 ++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 core/libs/tests/GuestFontScissor.cpp diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index dfa37bf121..5af35fd6b8 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -7,6 +7,16 @@ if(BUILD_TESTING) target_link_libraries(guest_font_tests PRIVATE libSceFont libSceFontFt libc) configure_windows_unwind(guest_font_tests) add_test(NAME guest_font COMMAND guest_font_tests) + add_test_executable(guest_font_scissor_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontScissor.cpp) + target_include_directories(guest_font_scissor_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_scissor_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_scissor_tests) + add_test(NAME guest_font_scissor COMMAND guest_font_scissor_tests) + set_tests_properties(guest_font_scissor PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_font_scissor PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFont>") + endif() add_test_executable(guest_system_font_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemFont.cpp) target_include_directories(guest_system_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_font_tests PRIVATE libSceFont libSceFontFt libc) diff --git a/core/libs/tests/GuestFontScissor.cpp b/core/libs/tests/GuestFontScissor.cpp new file mode 100644 index 0000000000..60b5c09c3e --- /dev/null +++ b/core/libs/tests/GuestFontScissor.cpp @@ -0,0 +1,225 @@ +#include "prx/libSceFontFt/include/Api.hpp" +#include "prx/libSceFont/include/Api.hpp" +#include "prx/libSceFont/include/FontTypes.hpp" +#include <algorithm> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <initializer_list> +#include <utility> +#include <vector> + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font scissor check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static int allocations = 0; +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + ++allocations; + return std::malloc(size); +} +static void APS5_VABI Release(void*, void* pointer) { + if (pointer) --allocations; + std::free(pointer); +} + +static void Put16(std::vector<unsigned char>& out, std::uint32_t value) { + out.push_back(static_cast<unsigned char>(value >> 8)); + out.push_back(static_cast<unsigned char>(value)); +} + +static void Put32(std::vector<unsigned char>& out, std::uint32_t value) { + Put16(out, value >> 16); + Put16(out, value & 0xFFFFu); +} + +static void PutAll16(std::vector<unsigned char>& out, std::initializer_list<int> values) { + for (const int value : values) Put16(out, static_cast<std::uint32_t>(static_cast<std::uint16_t>(value))); +} + +static std::vector<unsigned char> SquareGlyphFont() { + constexpr std::uint32_t codepoint = 'A'; + constexpr int width = 900; + std::vector<unsigned char> head; + Put32(head, 0x00010000u); + Put32(head, 0x00010000u); + Put32(head, 0); + Put32(head, 0x5F0F3CF5u); + PutAll16(head, {0x000B, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width, 800, 0, 8, 2, 0, 0}); + std::vector<unsigned char> hhea; + Put32(hhea, 0x00010000u); + PutAll16(hhea, {800, -200, 0, width, 0, 0, width, 1, 0, 0, 0, 0, 0, 0, 0, 2}); + std::vector<unsigned char> maxp; + Put32(maxp, 0x00010000u); + PutAll16(maxp, {2, 4, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); + std::vector<unsigned char> hmtx; + PutAll16(hmtx, {width, 0, width, 100}); + std::vector<unsigned char> glyf; + PutAll16(glyf, {1, 100, 0, width - 100, 700, 3, 0}); + glyf.insert(glyf.end(), {1, 1, 1, 1}); + PutAll16(glyf, {100, 0, width - 200, 0, 0, 700, 0, -700}); + std::vector<unsigned char> loca; + PutAll16(loca, {0, 0, static_cast<int>(glyf.size() / 2)}); + std::vector<unsigned char> cmap; + PutAll16(cmap, {0, 1, 3, 1}); + Put32(cmap, 12); + PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, static_cast<int>(codepoint), 0xFFFF, 0, static_cast<int>(codepoint), 0xFFFF, 1 - static_cast<int>(codepoint), 1, 0, 0}); + const std::vector<std::pair<const char*, const std::vector<unsigned char>*>> tables = { + {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; + std::vector<unsigned char> font; + Put32(font, 0x00010000u); + PutAll16(font, {static_cast<int>(tables.size()), 64, 2, static_cast<int>(tables.size()) * 16 - 64}); + std::uint32_t offset = 12 + static_cast<std::uint32_t>(tables.size()) * 16; + for (const auto& [tag, data] : tables) { + font.insert(font.end(), tag, tag + 4); + Put32(font, 0); + Put32(font, offset); + Put32(font, static_cast<std::uint32_t>(data->size())); + offset += (static_cast<std::uint32_t>(data->size()) + 3u) & ~3u; + } + for (const auto& [tag, data] : tables) { + font.insert(font.end(), data->begin(), data->end()); + font.resize((font.size() + 3u) & ~std::size_t{3}); + } + return font; +} + +static void CheckScissor(int width, int height, int x0, int y0, int w, int h, int expectedX0, int expectedY0, int expectedX1, int expectedY1) { + FontRenderSurface surface{}; + sceFontRenderSurfaceInit(&surface, nullptr, 0, 1, width, height); + sceFontRenderSurfaceSetScissor(&surface, x0, y0, w, h); + Require(surface.sc_x0 == expectedX0 && surface.sc_y0 == expectedY0); + Require(surface.sc_x1 == expectedX1 && surface.sc_y1 == expectedY1); +} + +static void CheckScissorArithmetic() { + CheckScissor(100, 80, 10, 20, 30, 40, 10, 20, 40, 60); + CheckScissor(100, 80, -10, -5, 30, 20, 0, 0, 20, 15); + CheckScissor(100, 80, -30, -20, 30, 20, 0, 0, 0, 0); + CheckScissor(100, 80, 90, 70, 30, 20, 90, 70, 100, 80); + CheckScissor(100, 80, 100, 80, 10, 10, 100, 80, 100, 80); + CheckScissor(100, 80, 101, 81, 10, 10, 100, 80, 100, 80); + CheckScissor(100, 80, 0, 0, 0, 0, 0, 0, 0, 0); + CheckScissor(100, 80, 50, 40, 0, 0, 50, 40, 50, 40); + CheckScissor(100, 80, 50, 40, 1000, 1000, 50, 40, 100, 80); + CheckScissor(100, 80, -1000, -1000, 1000, 1000, 0, 0, 0, 0); + CheckScissor(100, 80, -5, -5, 10, 10, 0, 0, 5, 5); + CheckScissor(1, 1, 0, 0, 2, 2, 0, 0, 1, 1); + CheckScissor(0, 80, 10, 20, 30, 40, 0, 20, 0, 60); + CheckScissor(100, 0, 10, 20, 30, 40, 10, 0, 40, 0); + + FontRenderSurface surface{}; + sceFontRenderSurfaceInit(&surface, nullptr, 0, 1, 0, 80); + surface.sc_x0 = 7; + surface.sc_x1 = 9; + sceFontRenderSurfaceSetScissor(&surface, 10, 20, 30, 40); + Require(surface.sc_x0 == 7 && surface.sc_x1 == 9); + Require(surface.sc_y0 == 20 && surface.sc_y1 == 60); + + sceFontRenderSurfaceInit(&surface, nullptr, 0, 1, 100, 0); + surface.sc_y0 = 3; + surface.sc_y1 = 5; + sceFontRenderSurfaceSetScissor(&surface, 10, 20, 30, 40); + Require(surface.sc_x0 == 10 && surface.sc_x1 == 40); + Require(surface.sc_y0 == 3 && surface.sc_y1 == 5); +} + +struct RenderedGlyph { + std::vector<std::uint8_t> pixels = std::vector<std::uint8_t>(128 * 128); + FontGlyphMetrics metrics{}; + FontRenderOutput output{}; +}; + +static RenderedGlyph RenderGlyph(FontHandle font, int x, int y, bool scissor, int x0 = 0, int y0 = 0, int w = 0, int h = 0) { + RenderedGlyph rendered; + FontRenderSurface surface{}; + sceFontRenderSurfaceInit(&surface, rendered.pixels.data(), 128, 1, 128, 128); + if (scissor) sceFontRenderSurfaceSetScissor(&surface, x0, y0, w, h); + Require(sceFontRenderCharGlyphImageHorizontal(font, 'A', &surface, static_cast<float>(x), static_cast<float>(y), &rendered.metrics, &rendered.output) == SCE_FONT_OK); + return rendered; +} + +static void CheckRenderedClipping(FontHandle font) { + const RenderedGlyph reference = RenderGlyph(font, 10, 100, false); + const auto& rect = reference.output.UpdateRect; + Require(rect.w > 4 && rect.h > 4); + Require(rect.x + rect.w <= 128 && rect.y + rect.h <= 128); + int lit = 0; + for (std::size_t index = 0; index < reference.pixels.size(); ++index) { + const int x = static_cast<int>(index % 128); + const int y = static_cast<int>(index / 128); + const bool inside = x >= static_cast<int>(rect.x) && x < static_cast<int>(rect.x + rect.w) && y >= static_cast<int>(rect.y) && y < static_cast<int>(rect.y + rect.h); + if (!inside) Require(reference.pixels[index] == 0); + if (reference.pixels[index] != 0) ++lit; + } + Require(lit > 0); + + const int insetX = static_cast<int>(rect.x) + 1; + const int insetY = static_cast<int>(rect.y) + 1; + const int insetW = static_cast<int>(rect.w) - 2; + const int insetH = static_cast<int>(rect.h) - 2; + const RenderedGlyph clipped = RenderGlyph(font, 10, 100, true, insetX, insetY, insetW, insetH); + Require(clipped.output.UpdateRect.x == static_cast<std::uint32_t>(insetX) && clipped.output.UpdateRect.y == static_cast<std::uint32_t>(insetY)); + Require(clipped.output.UpdateRect.w == static_cast<std::uint32_t>(insetW) && clipped.output.UpdateRect.h == static_cast<std::uint32_t>(insetH)); + for (std::size_t index = 0; index < clipped.pixels.size(); ++index) { + const int x = static_cast<int>(index % 128); + const int y = static_cast<int>(index / 128); + const bool inside = x >= insetX && x < insetX + insetW && y >= insetY && y < insetY + insetH; + Require(clipped.pixels[index] == (inside ? reference.pixels[index] : 0)); + } + + const int largerX = std::max(0, static_cast<int>(rect.x) - 5); + const int largerY = std::max(0, static_cast<int>(rect.y) - 5); + const int largerW = std::min(128 - largerX, static_cast<int>(rect.w) + 10); + const int largerH = std::min(128 - largerY, static_cast<int>(rect.h) + 10); + const RenderedGlyph larger = RenderGlyph(font, 10, 100, true, largerX, largerY, largerW, largerH); + Require(std::memcmp(&larger.output.UpdateRect, &rect, sizeof(rect)) == 0); + Require(larger.pixels == reference.pixels); + + const int negativeX = static_cast<int>(rect.x) - 10; + const int negativeY = static_cast<int>(rect.y) - 10; + const RenderedGlyph negative = RenderGlyph(font, 10, 100, true, negativeX, negativeY, static_cast<int>(rect.w) + 20, static_cast<int>(rect.h) + 20); + Require(std::memcmp(&negative.output.UpdateRect, &rect, sizeof(rect)) == 0); + Require(negative.pixels == reference.pixels); + + const RenderedGlyph empty = RenderGlyph(font, 10, 100, true, static_cast<int>(rect.x), static_cast<int>(rect.y), 0, 0); + Require(empty.output.UpdateRect.w == 0 && empty.output.UpdateRect.h == 0); + Require(std::all_of(empty.pixels.begin(), empty.pixels.end(), [](std::uint8_t value) { return value == 0; })); + Require(empty.metrics.width > 0.0f && empty.metrics.Horizontal.advance > 0.0f); + + const RenderedGlyph outside = RenderGlyph(font, 10, 100, true, 0, 0, 1, 1); + Require(outside.output.UpdateRect.w == 0 && outside.output.UpdateRect.h == 0); + Require(std::all_of(outside.pixels.begin(), outside.pixels.end(), [](std::uint8_t value) { return value == 0; })); + Require(outside.metrics.width > 0.0f && outside.metrics.Horizontal.advance > 0.0f); +} + +int main() { + CheckScissorArithmetic(); + + const FontMemoryInterface iface{Allocate, Release, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); + FontLibrary library = nullptr; + Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK); + Require(sceFontSupportExternalFonts(library, 1, 0x52) == SCE_FONT_OK); + const std::vector<unsigned char> fontData = SquareGlyphFont(); + FontHandle font = nullptr; + Require(sceFontOpenFontMemory(library, fontData.data(), static_cast<std::uint32_t>(fontData.size()), nullptr, &font) == SCE_FONT_OK); + Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); + FontRenderer renderer = nullptr; + Require(sceFontCreateRenderer(&memory, sceFontSelectRendererFt(0), &renderer) == SCE_FONT_OK); + Require(sceFontBindRenderer(font, renderer) == SCE_FONT_OK); + Require(sceFontSetupRenderScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); + CheckRenderedClipping(font); + Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); + Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK); + Require(sceFontCloseFont(font) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(allocations == 0); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); +} From ad9a68f9ba1ea905c12480dbeab98d8bd33dfb4d Mon Sep 17 00:00:00 2001 From: simnJS <simnkrystal@gmail.com> Date: Tue, 27 Oct 2026 17:50:24 +0100 Subject: [PATCH 553/702] feat(libSceNpMatching2): answer signaling connection status and context start abort offline sceNpMatching2SignalingGetConnectionStatus reports every member inactive for a known context, since no room is joined offline, and leaves the peer address and port untouched; a null status pointer throws. sceNpMatching2AbortContextStart returns NOT_INITIALIZED before initialization and 0 afterwards. Checks follow shadPS4, with RPCS3's differences recorded in the technical debt entry. --- core/libs/prx/libSceNpMatching2/Export.cpp | 20 ++++++--- .../prx/libSceNpMatching2/include/Api.hpp | 4 +- core/libs/tests/GuestNpMatching2Offline.cpp | 41 +++++++++++++++++++ docs/dev/debt/libSceNpMatching2.md | 5 ++- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNpMatching2/Export.cpp b/core/libs/prx/libSceNpMatching2/Export.cpp index 0884985aa7..c6a28b47b5 100644 --- a/core/libs/prx/libSceNpMatching2/Export.cpp +++ b/core/libs/prx/libSceNpMatching2/Export.cpp @@ -13,6 +13,7 @@ static constexpr int SCE_NP_MATCHING2_ERROR_CONTEXT_MAX = static_cast<int>(0x805 static constexpr int SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_STARTED = static_cast<int>(0x80550C08); static constexpr int SCE_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID = static_cast<int>(0x80550C0B); static constexpr int SCE_NET_ERROR_RESOLVER_ETIMEDOUT = static_cast<int>(0x804101E2); +static constexpr std::int32_t SCE_NP_SIGNALING_CONN_STATUS_INACTIVE = 0; static constexpr int MAX_CONTEXTS = 10; static constexpr std::uint64_t CREATE_CONTEXT_PARAMETER_SIZE = 0x28; @@ -215,9 +216,10 @@ int APS5_VABI sceNpMatching2Terminate(void) { return 0; } -int APS5_VABI sceNpMatching2AbortContextStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2AbortContextStart(std::uint16_t ctxId) { + (void)ctxId; + std::lock_guard lock(matching2Mutex); + return matching2Initialized ? 0 : SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; } int APS5_VABI sceNpMatching2CreateContextA(void) { @@ -380,8 +382,16 @@ int APS5_VABI sceNpMatching2SignalingGetConnectionInfoA(void) { return 0; } -int APS5_VABI sceNpMatching2SignalingGetConnectionStatus(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpMatching2SignalingGetConnectionStatus(std::uint16_t ctxId, std::uint64_t roomId, std::uint16_t memberId, std::int32_t* connStatus, void* peerAddr, std::uint16_t* peerPort) { + (void)roomId; + (void)memberId; + (void)peerAddr; + (void)peerPort; + std::lock_guard lock(matching2Mutex); + if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; + if (!connStatus) NotImplemented_nid_no_patch(__func__); + if (const int error = CheckContext(ctxId)) return error; + *connStatus = SCE_NP_SIGNALING_CONN_STATUS_INACTIVE; return 0; } diff --git a/core/libs/prx/libSceNpMatching2/include/Api.hpp b/core/libs/prx/libSceNpMatching2/include/Api.hpp index 74c4a2b2d7..e83dba6539 100644 --- a/core/libs/prx/libSceNpMatching2/include/Api.hpp +++ b/core/libs/prx/libSceNpMatching2/include/Api.hpp @@ -52,7 +52,7 @@ int APS5_VABI sceNpMatching2SetUserInfo(); int APS5_VABI sceNpMatching2SignalingGetConnectionInfo(); int APS5_VABI sceNpMatching2Terminate(void); -int APS5_VABI sceNpMatching2AbortContextStart(); +int APS5_VABI sceNpMatching2AbortContextStart(std::uint16_t ctxId); int APS5_VABI sceNpMatching2CreateContextA(); int APS5_VABI sceNpMatching2CreateContextInternal(); int APS5_VABI sceNpMatching2CreateJoinRoomA(); @@ -85,7 +85,7 @@ int APS5_VABI sceNpMatching2SignalingCancelPeerNetInfo(); int APS5_VABI sceNpMatching2SignalingEnableManualUdpMode(); int APS5_VABI sceNpMatching2SignalingEstablishConnection(); int APS5_VABI sceNpMatching2SignalingGetConnectionInfoA(); -int APS5_VABI sceNpMatching2SignalingGetConnectionStatus(); +int APS5_VABI sceNpMatching2SignalingGetConnectionStatus(std::uint16_t ctxId, std::uint64_t roomId, std::uint16_t memberId, std::int32_t* connStatus, void* peerAddr, std::uint16_t* peerPort); int APS5_VABI sceNpMatching2SignalingGetLocalNetInfo(); int APS5_VABI sceNpMatching2SignalingGetPeerNetInfo(); int APS5_VABI sceNpMatching2SignalingGetPeerNetInfoResult(); diff --git a/core/libs/tests/GuestNpMatching2Offline.cpp b/core/libs/tests/GuestNpMatching2Offline.cpp index 796c217bdf..f7cbf716f0 100644 --- a/core/libs/tests/GuestNpMatching2Offline.cpp +++ b/core/libs/tests/GuestNpMatching2Offline.cpp @@ -2,6 +2,8 @@ #include "SceTypes.hpp" #include <cstdint> #include <cstdio> +#include <stdexcept> +#include <string> @@ -15,6 +17,10 @@ constexpr int ContextMax = static_cast<int>(0x80550C04u); constexpr int ContextNotStarted = static_cast<int>(0x80550C08u); constexpr int InvalidArgument = static_cast<int>(0x80550C0Au); constexpr int InvalidContextId = static_cast<int>(0x80550C0Bu); +constexpr std::int32_t ConnStatusInactive = 0; +constexpr std::int32_t StatusSentinel = 0x5A5A5A5A; +constexpr std::uint32_t PeerAddrSentinel = 0xA5A5A5A5u; +constexpr std::uint16_t PeerPortSentinel = 0xA5A5; int failures = 0; @@ -24,6 +30,15 @@ void Require(bool value, const char* name, const char* what) { ++failures; } +bool NullStatusThrows(std::uint16_t ctxId) { + try { + sceNpMatching2SignalingGetConnectionStatus(ctxId, 1, 1, nullptr, nullptr, nullptr); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find("not implemented") != std::string::npos; + } + return false; +} + } int main() { @@ -37,6 +52,10 @@ int main() { Require(sceNpMatching2DestroyContext(1) == NotInitialized, "sceNpMatching2DestroyContext", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2ContextStart(1, 0) == NotInitialized, "sceNpMatching2ContextStart", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2ContextStop(1) == NotInitialized, "sceNpMatching2ContextStop", "a call before initialization did not return NOT_INITIALIZED"); + Require(sceNpMatching2AbortContextStart(1) == NotInitialized, "sceNpMatching2AbortContextStart", "a call before initialization did not return NOT_INITIALIZED"); + std::int32_t status = StatusSentinel; + Require(sceNpMatching2SignalingGetConnectionStatus(1, 1, 1, &status, nullptr, nullptr) == NotInitialized && status == StatusSentinel, "sceNpMatching2SignalingGetConnectionStatus", "a call before initialization did not return NOT_INITIALIZED without writing the status"); + Require(sceNpMatching2SignalingGetConnectionStatus(1, 1, 1, nullptr, nullptr, nullptr) == NotInitialized, "sceNpMatching2SignalingGetConnectionStatus", "a null status before initialization did not return NOT_INITIALIZED first"); Require(sceNpMatching2RegisterContextCallback(nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterContextCallback", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2RegisterRoomEventCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterRoomEventCallback", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2RegisterRoomMessageCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterRoomMessageCallback", "a call before initialization did not return NOT_INITIALIZED"); @@ -68,6 +87,25 @@ int main() { Require(sceNpMatching2ContextStop(1) == ContextNotStarted, "sceNpMatching2ContextStop", "a context that never started did not return CONTEXT_NOT_STARTED"); Require(sceNpMatching2ContextStop(99) == InvalidContextId, "sceNpMatching2ContextStop", "an unknown context was taken"); + Require(sceNpMatching2AbortContextStart(1) == 0, "sceNpMatching2AbortContextStart", "aborting the start of a context failed"); + Require(sceNpMatching2AbortContextStart(99) == 0, "sceNpMatching2AbortContextStart", "an unknown context id was refused"); + Require(sceNpMatching2ContextStop(1) == ContextNotStarted, "sceNpMatching2AbortContextStart", "the abort changed the start state of the context"); + + std::uint32_t peerAddr = PeerAddrSentinel; + std::uint16_t peerPort = PeerPortSentinel; + status = StatusSentinel; + Require(sceNpMatching2SignalingGetConnectionStatus(1, 0x1122334455667788ull, 3, &status, &peerAddr, &peerPort) == 0 && status == ConnStatusInactive, "sceNpMatching2SignalingGetConnectionStatus", "a member of a room that was never joined is not reported inactive"); + Require(peerAddr == PeerAddrSentinel && peerPort == PeerPortSentinel, "sceNpMatching2SignalingGetConnectionStatus", "the peer address or port was written for an inactive member"); + status = StatusSentinel; + Require(sceNpMatching2SignalingGetConnectionStatus(1, 0, 0, &status, nullptr, nullptr) == 0 && status == ConnStatusInactive, "sceNpMatching2SignalingGetConnectionStatus", "null peer address and port outputs were refused"); + status = StatusSentinel; + Require(sceNpMatching2SignalingGetConnectionStatus(99, 1, 1, &status, &peerAddr, &peerPort) == InvalidContextId && status == StatusSentinel, "sceNpMatching2SignalingGetConnectionStatus", "an unknown context was taken or its status written"); + Require(NullStatusThrows(1), "sceNpMatching2SignalingGetConnectionStatus", "a null status for a known context did not throw"); + Require(NullStatusThrows(99), "sceNpMatching2SignalingGetConnectionStatus", "a null status for an unknown context did not throw before the context check"); + Require(sceNpMatching2DestroyContext(2) == 0, "sceNpMatching2DestroyContext", "destroying context 2 failed"); + status = StatusSentinel; + Require(sceNpMatching2SignalingGetConnectionStatus(2, 1, 1, &status, nullptr, nullptr) == InvalidContextId && status == StatusSentinel, "sceNpMatching2SignalingGetConnectionStatus", "a destroyed context was taken"); + Require(sceNpMatching2RegisterContextCallback(nullptr, nullptr) == 0, "sceNpMatching2RegisterContextCallback", "registering the context callback failed"); Require(sceNpMatching2RegisterRoomEventCallback(99, nullptr, nullptr) == InvalidContextId, "sceNpMatching2RegisterRoomEventCallback", "an unknown context was taken"); Require(sceNpMatching2RegisterRoomEventCallback(1, nullptr, nullptr) == 0, "sceNpMatching2RegisterRoomEventCallback", "registering failed"); @@ -81,6 +119,9 @@ int main() { Require(sceNpMatching2Terminate() == 0, "sceNpMatching2Terminate", "termination failed"); Require(sceNpMatching2DestroyContext(1) == NotInitialized, "sceNpMatching2DestroyContext", "a context was destroyed after termination"); + Require(sceNpMatching2AbortContextStart(1) == NotInitialized, "sceNpMatching2AbortContextStart", "a call after termination did not return NOT_INITIALIZED"); + status = StatusSentinel; + Require(sceNpMatching2SignalingGetConnectionStatus(1, 1, 1, &status, nullptr, nullptr) == NotInitialized && status == StatusSentinel, "sceNpMatching2SignalingGetConnectionStatus", "a call after termination did not return NOT_INITIALIZED without writing the status"); Require(sceNpMatching2Initialize(&init) == 0, "sceNpMatching2Initialize", "initializing again failed"); Require(sceNpMatching2RegisterRoomEventCallback(5, nullptr, nullptr) == InvalidContextId, "sceNpMatching2Terminate", "a context created before termination is still found"); Require(sceNpMatching2CreateContext(&create, &created) == 0 && created == 1, "sceNpMatching2Terminate", "termination did not restart the context ids at 1"); diff --git a/docs/dev/debt/libSceNpMatching2.md b/docs/dev/debt/libSceNpMatching2.md index ea0493294f..396f6d4f0d 100644 --- a/docs/dev/debt/libSceNpMatching2.md +++ b/docs/dev/debt/libSceNpMatching2.md @@ -2,12 +2,13 @@ ### Unknown function info -- [sceNpMatching2AbortContextStart, sceNpMatching2CreateContextA, sceNpMatching2CreateContextInternal, sceNpMatching2CreateJoinRoomA, sceNpMatching2GetLobbyInfoList, sceNpMatching2GetLobbyMemberDataInternal, sceNpMatching2GetLobbyMemberDataInternalList, sceNpMatching2GetMemoryInfo, sceNpMatching2GetRoomJoinedSlotMaskLocal, sceNpMatching2GetRoomMemberDataExternalList, sceNpMatching2GetRoomMemberIdListLocal, sceNpMatching2GetRoomPasswordLocal, sceNpMatching2GetSignalingOptParamLocal](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpMatching2CreateContextA, sceNpMatching2CreateContextInternal, sceNpMatching2CreateJoinRoomA, sceNpMatching2GetLobbyInfoList, sceNpMatching2GetLobbyMemberDataInternal, sceNpMatching2GetLobbyMemberDataInternalList, sceNpMatching2GetMemoryInfo, sceNpMatching2GetRoomJoinedSlotMaskLocal, sceNpMatching2GetRoomMemberDataExternalList, sceNpMatching2GetRoomMemberIdListLocal, sceNpMatching2GetRoomPasswordLocal, sceNpMatching2GetSignalingOptParamLocal](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified - [sceNpMatching2GetSslMemoryInfo, sceNpMatching2GetUserInfoListA, sceNpMatching2GetWorldIdArrayForAllServers, sceNpMatching2GrantRoomOwner, sceNpMatching2JoinLobby, sceNpMatching2JoinRoomA, sceNpMatching2LeaveLobby, sceNpMatching2RegisterLobbyEventCallback, sceNpMatching2RegisterLobbyMessageCallback, sceNpMatching2RegisterManualUdpSignalingCallback, sceNpMatching2SendLobbyChatMessage, sceNpMatching2SetExtraInitParam, sceNpMatching2SetLobbyMemberDataInternal, sceNpMatching2SetRoomDataInternalExt](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified -- [sceNpMatching2SetSignalingOptParam, sceNpMatching2SignalingAbortConnection, sceNpMatching2SignalingCancelPeerNetInfo, sceNpMatching2SignalingEnableManualUdpMode, sceNpMatching2SignalingEstablishConnection, sceNpMatching2SignalingGetConnectionInfoA, sceNpMatching2SignalingGetConnectionStatus, sceNpMatching2SignalingGetLocalNetInfo, sceNpMatching2SignalingGetPeerNetInfo, sceNpMatching2SignalingGetPeerNetInfoResult, sceNpMatching2SignalingGetPingInfo, sceNpMatching2SignalingGetPort](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpMatching2SetSignalingOptParam, sceNpMatching2SignalingAbortConnection, sceNpMatching2SignalingCancelPeerNetInfo, sceNpMatching2SignalingEnableManualUdpMode, sceNpMatching2SignalingEstablishConnection, sceNpMatching2SignalingGetConnectionInfoA, sceNpMatching2SignalingGetLocalNetInfo, sceNpMatching2SignalingGetPeerNetInfo, sceNpMatching2SignalingGetPeerNetInfoResult, sceNpMatching2SignalingGetPingInfo, sceNpMatching2SignalingGetPort](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified - [sceNpMatching2SignalingSetPort](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified ### Functional - [sceNpMatching2Initialize, sceNpMatching2Terminate](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - only the library state is kept: initialization checks the parameter's `size` field (0x28, or 0x30 with an SSL pool) as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_matching2/np_matching2.cpp) does and starts no thread; a second initialization returns `ALREADY_INITIALIZED` and a termination without one `NOT_INITIALIZED`. The pool, thread and SSL settings are not used: no matching server is reached. Not measured on a console - [sceNpMatching2CreateContext and the context calls](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - offline only, not measured on a PS5: the checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_matching2/np_matching2.cpp#L26-L349) and its [context manager](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_matching2/np_matching2_internal.cpp#L1348-L1457) (a `0x28`-byte parameter, ten ids handed out round-robin). `sceNpMatching2ContextStart` returns `0x804101E2` (`SCE_NET_ERROR_RESOLVER_ETIMEDOUT`), which shadPS4 says was confirmed on a console disconnected from the internet, so no context starts and `sceNpMatching2ContextStop` answers `CONTEXT_NOT_STARTED`. Callbacks and the default request option are checked but never used. The room, user, world, server-id and signaling-info calls still throw +- [sceNpMatching2SignalingGetConnectionStatus, sceNpMatching2AbortContextStart](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - not measured on a console. With no room joined offline, the status call writes 0 (inactive) and returns 0 for a known context, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3eecea94ddaca360b19f4b800523b609a8b2ffcc/src/core/libraries/np/np_matching2/np_matching2.cpp#L569-L637) does; the PS3 module in [RPCS3](https://github.com/RPCS3/rpcs3/blob/c365ea76a552961ff078ea1a46da9d4b07729674/rpcs3/Emu/Cell/Modules/sceNp2.cpp#L449-L952) also writes inactive but returns `INVALID_ROOM_ID`. A null status pointer throws (shadPS4 skips the write, RPCS3 returns `INVALID_ARGUMENT`); peer address and port are never written. The abort returns `NOT_INITIALIZED` before initialization, as RPCS3 does, and 0 for any context id, as both do From 9b791df1636b5d66f269df7bcef62b1dfd0028ca Mon Sep 17 00:00:00 2001 From: slopgobblercobbler <slopgobblercobbler@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:06:21 +0100 Subject: [PATCH 554/702] test(relinker): add UTF-8 validation and JSON escaping tests for call registry writer --- .../tests/CallRegistryWriterTests.cpp | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/core/relinker/relinker/tests/CallRegistryWriterTests.cpp b/core/relinker/relinker/tests/CallRegistryWriterTests.cpp index 542f386b46..3113311b37 100644 --- a/core/relinker/relinker/tests/CallRegistryWriterTests.cpp +++ b/core/relinker/relinker/tests/CallRegistryWriterTests.cpp @@ -69,11 +69,143 @@ void RegistryOffsets() { } } +void RequireUtf8Failure(const std::string& input, const std::string& expectedMessage) { + Relinker::CallRegistryWriter writer; + try { + writer.WriteNeededLibraries({input}); + } catch (const Relinker::RelinkerException& error) { + Require(std::string(error.what()).find(expectedMessage) != std::string::npos, + "Expected message '" + expectedMessage + "', got: " + error.what()); + return; + } + throw std::runtime_error("Expected failure for input: " + input); +} + +void Utf8ValidationAndJsonEscaping() { + Relinker::CallRegistryWriter writer; + + for (const std::string& invalid : { + std::string("\x80", 1), + std::string("\xBF", 1), + std::string("\xC0", 1), + std::string("\xC1", 1), + std::string("\xF5", 1), + std::string("\xFF", 1), + std::string("prefix\x80postfix", 14), + std::string("lib\xC0.prx", 8) + }) { + RequireUtf8Failure(invalid, "Invalid UTF-8 lead byte in call registry string"); + } + + for (const std::string& truncated : { + std::string("\xC2", 1), + std::string("\xDF", 1), + std::string("\xE0", 1), + std::string("\xE2", 1), + std::string("\xE2\x82", 2), + std::string("\xF0", 1), + std::string("\xF0\x9F", 2), + std::string("\xF0\x9F\x98", 3), + std::string("hello\xC2", 6), + std::string("test\xE2\x82", 6) + }) { + RequireUtf8Failure(truncated, "Truncated UTF-8 sequence in call registry string"); + } + + for (const std::string& invalidContinuation : { + std::string("\xC2\x00", 2), + std::string("\xC2\x7F", 2), + std::string("\xC2\xC0", 2), + std::string("\xC2\x20", 2), + std::string("\xE2\x00\x80", 3), + std::string("\xE2\x82\x00", 3), + std::string("\xE2\x82\x7F", 3), + std::string("\xE2\x82\xC0", 3), + std::string("\xF0\x00\x80\x80", 4), + std::string("\xF0\x9F\x00\x80", 4), + std::string("\xF0\x9F\x98\x00", 4), + std::string("\xF0\x9F\x98\x7F", 4) + }) { + RequireUtf8Failure(invalidContinuation, "Invalid UTF-8 continuation byte in call registry string"); + } + + for (const std::string& invalidCodepoint : { + std::string("\xE0\x80\xAF", 3), + std::string("\xE0\x9F\xBF", 3), + std::string("\xF0\x80\x80\x80", 4), + std::string("\xF0\x8F\xBF\xBF", 4), + std::string("\xED\xA0\x80", 3), + std::string("\xED\xAD\xBF", 3), + std::string("\xED\xAE\x80", 3), + std::string("\xED\xBF\xBF", 3), + std::string("\xF4\x90\x80\x80", 4) + }) { + RequireUtf8Failure(invalidCodepoint, "Invalid UTF-8 codepoint in call registry string"); + } + + Relinker::CallRegistryEntry invalidEntry{}; + invalidEntry.Nid = std::string("\xFF", 1); + invalidEntry.CallSitesResolved = false; + try { + writer.WriteCallRegistry({invalidEntry}); + throw std::runtime_error("Expected failure in WriteCallRegistry for invalid UTF-8"); + } catch (const Relinker::RelinkerException& error) { + Require(std::string(error.what()).find("Invalid UTF-8 lead byte in call registry string") != std::string::npos, + "Expected lead byte error in WriteCallRegistry"); + } + + try { + writer.WriteModuleImports({invalidEntry}); + throw std::runtime_error("Expected failure in WriteModuleImports for invalid UTF-8"); + } catch (const Relinker::RelinkerException& error) { + Require(std::string(error.what()).find("Invalid UTF-8 lead byte in call registry string") != std::string::npos, + "Expected lead byte error in WriteModuleImports"); + } + + const std::string specialChars = "\"\\\n\r\t"; + const std::string specialJson = writer.WriteNeededLibraries({specialChars}); + Require(specialJson.find("\"\\\"\\\\\\n\\r\\t\"") != std::string::npos, "special characters are not escaped properly in JSON"); + + const std::string controlChars = std::string("\x00\x01\x07\x08\x0B\x0C\x1B\x1F", 8); + const std::string controlJson = writer.WriteNeededLibraries({controlChars}); + Require(controlJson.find("\"\\u0000\\u0001\\u0007\\u0008\\u000b\\u000c\\u001b\\u001f\"") != std::string::npos, + "control characters < 0x20 are not escaped to \\u00XX in JSON"); + + const std::string mixedEscapes = std::string("lib \"test\\v1\"\n\r\t\x07.prx", 21); + const std::string mixedJson = writer.WriteNeededLibraries({mixedEscapes}); + Require(mixedJson.find("\"lib \\\"test\\\\v1\\\"\\n\\r\\t\\u0007.prx\"") != std::string::npos, + "mixed escaping failed in JSON"); + + const std::string valid2Byte = "\xC2\xA2"; + const std::string valid3Byte = "\xE2\x82\xAC"; + const std::string valid4Byte = "\xF0\x9F\x98\x80"; + const std::string validCombined = "lib_\xC2\xA2_\xE2\x82\xAC_\xF0\x9F\x98\x80.prx"; + + const std::string validJson = writer.WriteNeededLibraries({valid2Byte, valid3Byte, valid4Byte, validCombined}); + Require(validJson.find(" \"" + valid2Byte + "\"") != std::string::npos, "valid 2-byte UTF-8 sequence was not preserved"); + Require(validJson.find(" \"" + valid3Byte + "\"") != std::string::npos, "valid 3-byte UTF-8 sequence was not preserved"); + Require(validJson.find(" \"" + valid4Byte + "\"") != std::string::npos, "valid 4-byte UTF-8 sequence was not preserved"); + Require(validJson.find(" \"" + validCombined + "\"") != std::string::npos, "combined valid UTF-8 sequences were not preserved"); + + Relinker::CallRegistryEntry validEntry{}; + validEntry.Nid = validCombined; + validEntry.CallSitesResolved = true; + validEntry.Library = valid2Byte; + validEntry.RelocationTypeString = valid3Byte; + validEntry.TargetSection = valid4Byte; + const std::string validRegistry = writer.WriteCallRegistry({validEntry}); + Require(validRegistry.find("\"nid\": \"" + validCombined + "\"") != std::string::npos, "NID UTF-8 not preserved in call registry"); + Require(validRegistry.find("\"library\": \"" + valid2Byte + "\"") != std::string::npos, "Library UTF-8 not preserved in call registry"); + Require(validRegistry.find("\"relocationType\": \"" + valid3Byte + "\"") != std::string::npos, "RelocationType UTF-8 not preserved in call registry"); + Require(validRegistry.find("\"targetSection\": \"" + valid4Byte + "\"") != std::string::npos, "TargetSection UTF-8 not preserved in call registry"); +} + } int main() { try { RegistryOffsets(); + Utf8ValidationAndJsonEscaping(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; From ef0bfaa5f2daaa245160f14a1783ba3b82dd655f Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 17:14:42 +0000 Subject: [PATCH 555/702] docs(libSceSaveData): record the sizes titles pass to sceSaveDataCreateTransactionResource Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceSaveData.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 0c4dfe7cc0..e36ce76844 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -6,7 +6,7 @@ ### Unknown function info -- [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state; resource parameter validation and transactional rollback semantics on host filesystem storage are unknown +- [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state; resource parameter validation and transactional rollback semantics on host filesystem storage are unknown. Nine of the ten local titles call `sceSaveDataCreateTransactionResource`, at 15 sites: ten pass a size of 0xC0000 (768 KiB), four pass 0 (PPSA03984, PPSA25286 and both PPSA34502 sites), and one PPSA18194 site passes a value loaded from a global - [sceSaveDataPrepareForTransferring](../../../core/libs/prx/libSceSaveData.native/Export.cpp): the native wrapper confirms the same 16-byte mount point and 40-byte preparation parameter transport as `sceSaveDataPrepare`. Transfer preparation still requires the save-data service backend; its protected stub is unchanged. - [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 - [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 queues a successful type-5 completion event with the user and directory; PPSA03671 polls for this event after conversion. 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 From 47436177010e124fdb21c6a119ed877b91aacef9 Mon Sep 17 00:00:00 2001 From: JulesB40 <jules.barrandon05@gmail.com> Date: Tue, 27 Oct 2026 18:08:11 +0100 Subject: [PATCH 556/702] test(tools): skip the configure seed CMake round trip without Ninja The round trip configures with -G Ninja but only skipped when CMake was missing, so it errored on hosts without Ninja. Require both, like the other configure seed project tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- tools/tests/test_configure_seed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 373ce43764..d6f1b74c3d 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -312,7 +312,7 @@ class ChecksParsingTests(unittest.TestCase): "SINGLE": ("", "Test SINGLE"), }) - @unittest.skipUnless(shutil.which("cmake"), "CMake is required for cache round trips") + @unittest.skipUnless(shutil.which("cmake") and shutil.which("ninja"), "CMake and Ninja are required for cache round trips") def test_multiline_help_from_real_cmake_survives_seed_round_trip(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) From 12b8c82aa50da06ce6bef3be183694d3f05d5b71 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 18:17:49 +0100 Subject: [PATCH 557/702] test(libSceTextToSpeech2): cover null initialize and status queries in GuestTextToSpeech2 --- core/libs/tests/GuestTextToSpeech2.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestTextToSpeech2.cpp b/core/libs/tests/GuestTextToSpeech2.cpp index 13cab7e16c..c6ba365c6e 100644 --- a/core/libs/tests/GuestTextToSpeech2.cpp +++ b/core/libs/tests/GuestTextToSpeech2.cpp @@ -8,10 +8,25 @@ int main() { constexpr std::int32_t unsupported = static_cast<std::int32_t>(0x8002002D); const std::uint32_t param[16]{0x2000000, 0x26c}; - if (sceTextToSpeech2Initialize(reinterpret_cast<const TextToSpeechInitParameter*>(param)) != unsupported) { + if (sceTextToSpeech2Initialize(reinterpret_cast<const TextToSpeechInitParameter*>(param)) != unsupported || + sceTextToSpeech2Initialize(nullptr) != unsupported) { std::puts("TextToSpeech2: initialization did not report the unsupported operation"); std::abort(); } + + std::int32_t status = 0x55AA; + if (sceTextToSpeech2GetSystemStatus(&status) != unsupported || status != 0x55AA || + sceTextToSpeech2GetSpeechStatus(&status) != unsupported || status != 0x55AA) { + std::puts("TextToSpeech2: status queries did not preserve the output value"); + std::abort(); + } + + const TextToSpeechOpenParameter openParam{}; + const TextToSpeechSpeakParameter speakParam{}; + if (sceTextToSpeech2Open(&openParam) != unsupported || sceTextToSpeech2Speak(&speakParam) != unsupported) { + std::puts("TextToSpeech2: configured open or speak did not report the unsupported operation"); + std::abort(); + } if (sceTextToSpeech2Open(nullptr) != unsupported || sceTextToSpeech2Cancel() != unsupported || sceTextToSpeech2Close() != unsupported || sceTextToSpeech2GetSpeechStatus(nullptr) != unsupported || sceTextToSpeech2Speak(nullptr) != unsupported || sceTextToSpeech2Terminate() != unsupported || From 79d65956a182ed5f130e38d4eed9e51247f824a1 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 27 Oct 2026 13:16:52 -0400 Subject: [PATCH 558/702] fix(libc): compute _Exp and _FExp as titles do The libc.prx that titles ship evaluates the rational step as ((Q(z) + g*P(z)) + 1) / ((Q(z) - g*P(z)) + 1), with its own ln 2 split and coefficients in the double case, and returns 1 only for arguments within half an epsilon of zero. main used a different rational form with a doubling step and an epsilon-wide shortcut, which left about one result in five one ulp away from titles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libs/prx/libc/src/DinkumwareInternals.cpp | 31 +++++++++---------- core/libs/tests/GuestDinkumwareInternals.cpp | 8 +++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp index dc45984759..fd7ee6ec4f 100644 --- a/core/libs/prx/libc/src/DinkumwareInternals.cpp +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -205,13 +205,12 @@ short APS5_VABI _Exp_nid_postfix(double* value, double y, short offset) { constexpr double HugeExp = 1842.0; constexpr double Eps = std::numeric_limits<double>::epsilon(); constexpr double InverseLn2 = 1.4426950408889634073599246810018921; - constexpr double Ln2High = 22713.0 / 32768.0; - constexpr double Ln2Low = 1.4286068203094172321214581765680755e-6; - constexpr double P1 = 420.30235984910635; - constexpr double P2 = 15132.70094680474802; - constexpr double Q0 = 30.01511290683317; - constexpr double Q1 = 3362.72154416553028; - constexpr double Q2 = 30265.40189360949691; + constexpr double Ln2High = 0x1.62e42f8p-1; + constexpr double Ln2Low = 0x1.be8e7bcd5e4f2p-27; + constexpr double P0 = 0x1.152b741a5e84bp-15; + constexpr double P1 = 0x1.c70e8daf3bd0bp-7; + constexpr double Q0 = 0x1.03fa081572e11p-10; + constexpr double Q1 = 0x1.c718f8c124358p-4; constexpr double E = 0x1.5bf0a8b145769p+1; const short xKind = ClassifyValue(x); const short yKind = ClassifyValue(y); @@ -277,14 +276,13 @@ short APS5_VABI _Exp_nid_postfix(double* value, double y, short offset) { g = static_cast<double>(exponent); g = (x - g * Ln2High) - g * Ln2Low; double factor; - if (-Eps < g && g < Eps) { + if (-Eps * 0.5 < g && g < Eps * 0.5) { factor = 1.0; } else { const double z = g * g; - const double w = (Q0 * z + Q1) * z + Q2; - g *= (z + P1) * z + P2; - factor = (w + g) / (w - g) * 2.0; - --exponent; + const double w = (Q0 * z + Q1) * z; + g *= (P0 * z + P1) * z + 0.5; + factor = (w + g + 1.0) / (w - g + 1.0); } const short result = ScaleProduct(*value, factor, y, exponent + offset); if (result == Finite && std::fpclassify(*value) == FP_SUBNORMAL) { @@ -373,14 +371,13 @@ short APS5_VABI _FExp_nid_postfix(float* value, float y, short offset) { g = static_cast<float>(exponent); g = (x - g * Ln2High) - g * Ln2Low; float factor; - if (-Eps < g && g < Eps) { + if (-Eps * 0.5f < g && g < Eps * 0.5f) { factor = 1.0f; } else { const float z = g * g; - const float w = Q * z + 1.0f; - g *= P0 + P1 * z; - factor = (w + g) / (w - g) * 2.0f; - --exponent; + const float w = Q * z; + g *= P1 * z + P0; + factor = (w + g + 1.0f) / (w - g + 1.0f); } const short result = ScaleProduct(*value, factor, y, exponent + offset); if (result == Finite && std::fpclassify(*value) == FP_SUBNORMAL) { diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp index c1be3d457e..fc64c245ea 100644 --- a/core/libs/tests/GuestDinkumwareInternals.cpp +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -253,6 +253,10 @@ void TestDoubleHelpers() { CheckExp(0.0, 1.0, 2000, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_Exp scaled overflow"); CheckExp(1842.0, 1.0, 0, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_Exp inside the positive cutoff overflows"); CheckExp(-1842.0, 1.0, 0, 0.0, Zero, ERANGE, FE_UNDERFLOW | FE_INEXACT, "_Exp inside the negative cutoff underflows"); + CheckExp(-0x1.a3ae147ae147bp+3, 1.0, 3, 0x1.0e6b5a0a65117p-16, Finite, 0, FE_INEXACT, "_Exp rounds a negative exponent as titles do"); + CheckExp(-0x1.e1fae147ae148p+6, 2.0, 3, 0x1.1e838b63af9c1p-170, Finite, 0, FE_INEXACT, "_Exp rounds a scaled small result as titles do"); + CheckExp(0x1.b073b645a1cacp+7, -1.0, -1, -0x1.edef67d6786cep+310, Finite, 0, FE_INEXACT, "_Exp rounds a large negative product as titles do"); + CheckExp(0x1.5c523eb57b55fp-53, 2.0, 0, 0x1.0000000000001p+1, Finite, 0, FE_INEXACT, "_Exp evaluates arguments above half an epsilon"); errno = 0; std::feclearexcept(FE_ALL_EXCEPT); @@ -340,6 +344,10 @@ void TestDoubleHelpers() { CheckFExp(0.0f, 1.0f, 200, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_FExp scaled overflow"); CheckFExp(229.0f, 1.0f, 0, INFINITY, Infinite, ERANGE, FE_OVERFLOW | FE_INEXACT, "_FExp inside the positive cutoff overflows"); CheckFExp(-229.0f, 1.0f, 0, 0.0f, Zero, ERANGE, FE_UNDERFLOW | FE_INEXACT, "_FExp inside the negative cutoff underflows"); + CheckFExp(-0x1.4fbe76p+0f, 1.0f, 3, 0x1.13e1bp+1f, Finite, 0, FE_INEXACT, "_FExp rounds a negative exponent as titles do"); + CheckFExp(-0x1.fd4b92p+5f, 1.0f, -3, 0x1.1d1806p-95f, Finite, 0, FE_INEXACT, "_FExp rounds a scaled small result as titles do"); + CheckFExp(0x1.5e147ap+4f, 2.0f, 3, 0x1.7b078cp+35f, Finite, 0, FE_INEXACT, "_FExp rounds a large product as titles do"); + CheckFExp(0x1.0644b8p-24f, 0.25f, 2, 0x1.000002p+0f, Finite, 0, FE_INEXACT, "_FExp evaluates arguments above half an epsilon"); errno = 0; std::feclearexcept(FE_ALL_EXCEPT); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d272adb019..fd9bc7c28f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -190,7 +190,7 @@ - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) - [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux and macOS have no crash report, so they print the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds. An arm64 libc build omits the recorder - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. -- [_Exp and _FExp](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow PRAGMATA PPSA02530's bundled `libc.prx` (static disassembly): both operands are classified first and only finite/denormal pairs take the finite path. The strict x-only cutoffs at ±229/±1842 store `0.0*y` without an error below and `y*inf` with overflow above, so an offset does not compensate them. A zero multiplier keeps its sign, a NaN multiplier leaves `*px` unchanged, `+inf` with zero or `-inf` with infinity is invalid, and finite multipliers with `+inf`/`-inf` return signed infinity/zero without flags. Inside the cutoffs subnormals are `Finite` without flags, a zero from scaling raises underflow, and the negative cutoff itself raises nothing. `_FExp` uses the measured `c1`, `c2`, `invln2`, `Q` and `P`; `_Exp` finite constants, exact finite bits, signaling NaNs, directed rounding and flush-to-zero modes remain unverified. +- [_Exp and _FExp](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) - signaling NaNs, directed rounding and flush-to-zero modes are unverified, and titles whose `libc.prx` contracts multiply-adds to FMA, such as PPSA13934, differ by one unit in the last place on 889 `_FExp` and 15 `_Exp` of 300,000 random calls. Finite results use the constants, evaluation order and half-epsilon shortcut of the `libc.prx` that titles ship and match that of PPSA04158, PPSA05916, PPSA07923 and PPSA20423 on those calls, run on a Windows host. Special operands follow PRAGMATA PPSA02530's bundled `libc.prx`: the x-only cutoffs at ±229/±1842 store `0.0*y` below and `y*inf` with overflow above, ignoring the offset; a zero multiplier keeps its sign, a NaN multiplier leaves `*px` unchanged, and `+inf` with zero or `-inf` with infinity is invalid. - [exp2f](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_exp2f.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_exp2f.c), with `huge` and `twom100` read at run time and `x + redux` rounded to float, as msun's `volatile` reads and `STRICT_ASSIGN` do. As in the `libc.prx` that titles ship, `errno` is set to `ERANGE` only in msun's overflow (`x >= 128`) and underflow (`x <= -150`) branches, so a subnormal result leaves it unchanged. Floating-point exceptions are not compared. It gives the same bits and `errno` as the `exp2f` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified From 0fdce2faf4684703a07bdc2dddbc724305ddd33c Mon Sep 17 00:00:00 2001 From: Cankiee <13728354+Cankiee@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:22:57 +0100 Subject: [PATCH 559/702] fix(tools): reject duplicate timeline buttons --- tools/pad_timeline.py | 2 ++ tools/tests/test_pad_timeline.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/tools/pad_timeline.py b/tools/pad_timeline.py index 16672d228e..e54225f807 100644 --- a/tools/pad_timeline.py +++ b/tools/pad_timeline.py @@ -38,6 +38,8 @@ def compile_timeline(document, sequence, repeat=1, gap_ms=200): names = event["buttons"] if not isinstance(names, list) or not all(isinstance(name, str) and name in BUTTONS for name in names): raise ValueError(f"{prefix}.buttons must be a list of known button names") + if len(set(names)) != len(names): + raise ValueError(f"{prefix}.buttons must not contain duplicate button names") if "release" in names and len(names) != 1: raise ValueError(f"{prefix}: release cannot be combined with another button") buttons = 0 diff --git a/tools/tests/test_pad_timeline.py b/tools/tests/test_pad_timeline.py index b7a91f3d96..60043b2c44 100644 --- a/tools/tests/test_pad_timeline.py +++ b/tools/tests/test_pad_timeline.py @@ -40,6 +40,24 @@ class TimelineTests(unittest.TestCase): with self.assertRaises(ValueError): pad_timeline.compile_timeline(self.document, 1) + def test_duplicate_buttons_are_rejected(self): + document = { + "version": 1, + "events": [ + {"at_ms": 0, "hold_ms": 100, "buttons": ["cross", "cross"]} + ], + } + + for buttons in ( + ["cross", "cross"], + ["l1", "r1", "l1"], + ["release", "release"], + ): + with self.subTest(buttons=buttons): + document["events"][0]["buttons"] = buttons + with self.assertRaisesRegex(ValueError, "duplicate button names"): + pad_timeline.compile_timeline(document, sequence=1) + def test_unknown_buttons(self): for buttons in [["typo"], "cross", [True]]: with self.subTest(buttons=buttons): From 22e89180f68dedd9c766dbce65801752b11cc07e Mon Sep 17 00:00:00 2001 From: Cankiee <13728354+Cankiee@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:39:03 +0100 Subject: [PATCH 560/702] fix(tools): match phase kind when settling --- tools/ab_split.py | 2 +- tools/tests/test_ab_split.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tools/ab_split.py b/tools/ab_split.py index 4bedf518ea..b3c76e0360 100644 --- a/tools/ab_split.py +++ b/tools/ab_split.py @@ -75,7 +75,7 @@ def read_log(lines): current = {"kind": kind, "index": index, "begin": (present, seconds), "settled": None, "end": None, "reports": {name: [] for name, _ in KINDS}} phases.append(current) - elif current is not None and current["index"] == index: + elif current is not None and current["index"] == index and current["kind"] == kind: current["settled"] = (present, seconds) continue for name, prefix in KINDS: diff --git a/tools/tests/test_ab_split.py b/tools/tests/test_ab_split.py index 319872dc3d..ba399c3626 100644 --- a/tools/tests/test_ab_split.py +++ b/tools/tests/test_ab_split.py @@ -62,6 +62,25 @@ def log(): class AbSplitTests(unittest.TestCase): + def test_settled_phase_requires_matching_kind_and_index(self): + lines = [ + "[ab] phase A 0 begins at present 100 (t 10.000 s)\n", + "[ab] phase B 0 settled at present 102 (t 12.000 s)\n", + ] + + _, phases = ab_split.read_log(lines) + + self.assertEqual(len(phases), 1) + self.assertEqual(phases[0]["kind"], "A") + self.assertIsNone(phases[0]["settled"]) + + lines.append( + "[ab] phase A 0 settled at present 103 (t 13.000 s)\n" + ) + + _, phases = ab_split.read_log(lines) + self.assertEqual(phases[0]["settled"], (103, 13.0)) + def test_phases_count_only_reports_inside_a_settled_phase(self): result = ab_split.split(log(), ["0x1000"], ["staging-in"]) self.assertTrue(result["config"].startswith("[ab] in-run A/B: phases of 30.0 s")) From 552d38908301cdbf6cb8a41ce7256de121f65a23 Mon Sep 17 00:00:00 2001 From: Andy <68301335+Lancito01@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:01:47 -0300 Subject: [PATCH 561/702] test(agc): disable barycentric built-ins in the mesh barycentric emulation test #9193 added fragmentBarycentricBuiltins and cleared it in PixelBarycentricEmulation, but MeshBarycentricEmulation still takes the flag from the device. On a device with VK_KHR_fragment_shader_barycentric the host interpolation check then reads I/J from BaryCoordKHR, the pixel shader does not emulate barycentrics, and the test fails before its first draw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp index 58f20d44f9..9e1f7f4a4a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp @@ -115,6 +115,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::spa Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; + target.fragmentBarycentricBuiltins = false; const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Vertices.size()), 0, 1, 0, false}; std::vector<std::uint32_t> meshUserData(12, 0u); From 56be4608fdbaebe9a464440b2b7ac1aa70a724e4 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 27 Oct 2026 14:00:42 -0400 Subject: [PATCH 562/702] fix(nid): move PE library imports whose NID is longer than the name A library's imports were renamed in place, so any import whose NID is longer than its name failed the build: libSceHttp2 importing sceNetRecv (10 characters, NID 11) stopped at "import NID longer than original name". Such imports now get their hint/name entry in the appended .nidimp section that --exec mode already uses, with both thunks repointed; names that fit are still patched in place, so a library without short imports keeps its layout. Signed, bound and delay-loaded imports are rejected before anything is written whenever names move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/nid/src/PeNidPatcher.cpp | 32 ++++++------ core/libs/nid/tests/test_pe_references.py | 64 +++++++++++++++++++++++ 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/core/libs/nid/src/PeNidPatcher.cpp b/core/libs/nid/src/PeNidPatcher.cpp index 638f3bb29e..564d23aa22 100644 --- a/core/libs/nid/src/PeNidPatcher.cpp +++ b/core/libs/nid/src/PeNidPatcher.cpp @@ -253,12 +253,6 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l std::uint32_t FirstThunk; }; - if (options.executable) { - for (const auto index : {4u, 11u, 13u}) { - const auto directory = Read<PeDataDirectory>(pe, dataDirectoryOffset + index * sizeof(PeDataDirectory)); - if (directory.VirtualAddress || directory.Size) throw std::runtime_error("signed, bound or delay-loaded PE imports are not supported in --exec mode"); - } - } struct ImportUse { std::size_t NameOffset; std::size_t LookupOffset; @@ -315,19 +309,27 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l importDescOffset += sizeof(PeImportDescriptor); } const auto importMap = ResolveImportNids(importNames, excludedExports, options); - if (!options.executable) { - for (const auto& item : imports) { - const auto& name = importMap.at(item.Name); - if (name.size() > item.Name.size()) throw std::runtime_error("import NID longer than original name: " + item.Name); - std::memcpy(pe.data() + item.NameOffset, name.c_str(), name.size() + 1); - } - return; + const auto fitsInPlace = [&](const ImportUse& item) { + return !options.executable && importMap.at(item.Name).size() <= item.Name.size(); + }; + const bool growsImports = options.executable || std::any_of(imports.begin(), imports.end(), [&](const ImportUse& item) { + return !fitsInPlace(item) && importMap.at(item.Name) != item.Name; + }); + for (const auto index : {4u, 11u, 13u}) { + const auto directory = Read<PeDataDirectory>(pe, dataDirectoryOffset + index * sizeof(PeDataDirectory)); + if (growsImports && (directory.VirtualAddress || directory.Size)) + throw std::runtime_error("signed, bound or delay-loaded PE imports are not supported when import names move to a new section"); + } + for (const auto& item : imports) { + if (!fitsInPlace(item)) continue; + const auto& name = importMap.at(item.Name); + std::memcpy(pe.data() + item.NameOffset, name.c_str(), name.size() + 1); } std::vector<std::uint8_t> strings; std::unordered_map<std::string, std::uint32_t> stringOffsets; for (const auto& item : imports) { const auto& name = importMap.at(item.Name); - if (name == item.Name || stringOffsets.contains(name)) continue; + if (name == item.Name || fitsInPlace(item) || stringOffsets.contains(name)) continue; if (strings.size() & 1u) strings.push_back(0); if (strings.size() > 0xffffffffu - name.size() - 3u) throw std::runtime_error("PE import string table overflow"); stringOffsets.emplace(name, static_cast<std::uint32_t>(strings.size())); @@ -372,7 +374,7 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l Write(pe, optionalHeaderOffset + 64u, std::uint32_t{0}); for (const auto& item : imports) { const auto& name = importMap.at(item.Name); - if (name == item.Name) continue; + if (name == item.Name || fitsInPlace(item)) continue; const auto rva = section.VirtualAddress + stringOffsets.at(name); for (const auto offset : {item.LookupOffset, item.AddressOffset}) { if (magic == 0x20bu) Write(pe, offset, std::uint64_t{rva}); diff --git a/core/libs/nid/tests/test_pe_references.py b/core/libs/nid/tests/test_pe_references.py index 2992a55093..1368a3cac8 100644 --- a/core/libs/nid/tests/test_pe_references.py +++ b/core/libs/nid/tests/test_pe_references.py @@ -4,6 +4,7 @@ import sys import tempfile import unittest from pathlib import Path +from types import SimpleNamespace from test_macho_nids import build_dylib, exports_of, nid, PATCHER @@ -441,5 +442,68 @@ class ExecutableImportTests(unittest.TestCase): self.assertEqual(path.read_bytes(), image) +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") +class LibraryImportTests(unittest.TestCase): + def library(self, names): + image = ExecutableImportTests.pe(SimpleNamespace(names=names)) + struct.pack_into("<IIHHIIIIIII", image, 0x600, 0, 0, 0, 0, 0x1446, 1, 1, 1, 0x1428, 0x142c, 0x1430) + struct.pack_into("<IIH", image, 0x628, 0x1000, 0x1432, 0) + image[0x632:0x643] = b"puts_nid_postfix\0" + image[0x646:0x64e] = b"lib.dll\0" + struct.pack_into("<II", image, 0x58 + 112, 0x1400, 80) + return image + + def imports(self, image, count): + sections = [struct.unpack_from("<8sIIIIIIHHI", image, 0x58 + 240 + index * 40) + for index in range(struct.unpack_from("<H", image, 0x46)[0])] + result = [] + for index in range(count): + rva = struct.unpack_from("<Q", image, 576 + index * 8)[0] + self.assertEqual(rva, struct.unpack_from("<Q", image, 704 + index * 8)[0]) + section = next(s for s in sections if s[2] <= rva < s[2] + s[3]) + offset = section[4] + rva - section[2] + 2 + result.append((rva, image[offset:image.index(0, offset)].decode())) + return result, len(sections) + + def patch(self, image): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "library.dll" + path.write_bytes(image) + result = subprocess.run([str(PATCHER), "libtest", str(path)], capture_output=True, text=True, timeout=20) + return result, path.read_bytes() + + def test_imports_longer_as_nids_move_to_a_new_section(self): + names = ["sceA", "sceNetRecv", "sceKernelLongerName", "plain"] + image = self.library(names) + before, _ = self.imports(image, len(names)) + result, patched = self.patch(image) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + after, sections = self.imports(patched, len(names)) + self.assertEqual([name for _, name in after], [nid("sceA"), nid("sceNetRecv"), nid("sceKernelLongerName"), "plain"]) + self.assertEqual(sections, 2) + self.assertEqual([rva == old for (rva, _), (old, _) in zip(after, before)], [False, False, True, True]) + + def test_imports_that_fit_keep_the_section_layout(self): + names = ["sceKernelLongerName", "plain"] + result, patched = self.patch(self.library(names)) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + after, sections = self.imports(patched, len(names)) + self.assertEqual([name for _, name in after], [nid("sceKernelLongerName"), "plain"]) + self.assertEqual(sections, 1) + + def test_delay_loaded_imports_cannot_grow(self): + for names, accepted in ((["sceNetRecv"], False), (["sceKernelLongerName"], True)): + with self.subTest(names=names): + image = self.library(names) + struct.pack_into("<II", image, 0x58 + 112 + 13 * 8, 0x1500, 32) + result, patched = self.patch(image) + if accepted: + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + else: + self.assertNotEqual(result.returncode, 0) + self.assertIn("delay-loaded", result.stdout + result.stderr) + self.assertEqual(patched, image) + + if __name__ == "__main__": unittest.main() From 04bdc8c7e7ec3433bc5aec8403c8d742e592f257 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 18:16:54 +0100 Subject: [PATCH 563/702] test(libSceScreenShot): cover positive IDs in sceScreenShotIsDisabled Cover positive IDs 1 and 2 in sceScreenShotIsDisabled in GuestScreenShot. --- core/libs/tests/GuestScreenShot.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestScreenShot.cpp b/core/libs/tests/GuestScreenShot.cpp index 47e86f8874..d947dd67e1 100644 --- a/core/libs/tests/GuestScreenShot.cpp +++ b/core/libs/tests/GuestScreenShot.cpp @@ -67,6 +67,8 @@ void TestDisableState() { Require(sceScreenShotIsDisabled(-1, &value) == ErrorArgument && value == 7, "id -1 is rejected"); Require(sceScreenShotIsDisabled(0, nullptr) == ErrorArgument, "null result is rejected"); Require(sceScreenShotDisableNotification() == 0 && sceScreenShotEnableNotification() == 0, "notification toggles"); + Require(sceScreenShotIsDisabled(1, &value) == 0 && value == 0, "positive id 1 is accepted"); + Require(sceScreenShotIsDisabled(2, &value) == 0 && value == 0, "positive id 2 is accepted"); } void TestOverlay(bool large) { From a6beaf3acc1a1a2ce0f44fdb3b479960e62f0069 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 18:16:44 +0100 Subject: [PATCH 564/702] test(libSceSulphaDrv): expand sceSulphaGetDefaultConfig validation in GuestSulphaConfig Cover errno preservation across multiple values, exact-size 24-byte and raw 64-bit word buffers, idempotent calls, and expanded alignment/pattern grid. --- core/libs/tests/GuestSulphaConfig.cpp | 70 +++++++++++++++++++-------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/core/libs/tests/GuestSulphaConfig.cpp b/core/libs/tests/GuestSulphaConfig.cpp index 007930e68a..897771c754 100644 --- a/core/libs/tests/GuestSulphaConfig.cpp +++ b/core/libs/tests/GuestSulphaConfig.cpp @@ -14,9 +14,12 @@ static void Require(bool condition, const char* message) { } int main() { - errno = 7; - Require(sceSulphaGetDefaultConfig(nullptr) == static_cast<int>(0x804d0003u) && errno == 7, - "A null configuration must return the native argument error without changing errno"); + for (const int expectedErrno : {0, 7, 22, 42}) { + errno = expectedErrno; + Require(sceSulphaGetDefaultConfig(nullptr) == static_cast<int>(0x804d0003u) && errno == expectedErrno, + "A null configuration must return the native argument error without changing errno"); + } + struct Config { std::uint64_t size; std::uint64_t capacity; @@ -24,23 +27,52 @@ int main() { std::uint32_t port; }; static_assert(sizeof(Config) == 24); - for (const auto shift : {0u, 1u}) for (const auto initial : std::array<std::uint64_t, 4>{0, 24, 560, UINT64_MAX}) { - for (const unsigned char pattern : {0xa5, 0x5a}) { - std::array<unsigned char, 48> storage; - storage.fill(pattern); - auto* config = storage.data() + 8 + shift; - std::memcpy(config, &initial, sizeof(initial)); - errno = 7; - Require(sceSulphaGetDefaultConfig(config) == 0 && errno == 7, - "Default configuration must accept non-null storage regardless of its incoming size"); - Config result; - std::memcpy(&result, config, sizeof(result)); - Require(result.size == 24 && result.capacity == 4194304 && result.setting == 3 && result.port == 4001, - "Default configuration must match the native 24-byte record"); - for (std::size_t i = 0; i < storage.size(); ++i) { - if (i < 8 + shift || i >= 8 + shift + sizeof(Config)) - Require(storage[i] == pattern, "Default configuration must preserve bytes outside its record"); + + std::array<std::uint8_t, 24> exactStorage; + exactStorage.fill(0xee); + errno = 7; + Require(sceSulphaGetDefaultConfig(exactStorage.data()) == 0 && errno == 7, + "Default configuration must accept exact 24-byte buffer without changing errno"); + Config exactResult; + std::memcpy(&exactResult, exactStorage.data(), sizeof(exactResult)); + Require(exactResult.size == 24 && exactResult.capacity == 4194304 && exactResult.setting == 3 && exactResult.port == 4001, + "Default configuration in exact 24-byte buffer must match expected fields"); + + std::uint64_t rawWords[3]{0, 0, 0}; + Require(sceSulphaGetDefaultConfig(rawWords) == 0, + "Default configuration must accept 64-bit array destination"); + Require(rawWords[0] == 24ULL && rawWords[1] == 0x400000ULL && rawWords[2] == 0xFA100000003ULL, + "Default configuration raw 64-bit words must match native defaults"); + + Config idempotentConfig{}; + Require(sceSulphaGetDefaultConfig(&idempotentConfig) == 0, + "First call to default configuration must succeed"); + Require(sceSulphaGetDefaultConfig(&idempotentConfig) == 0, + "Second call to default configuration must succeed idempotently"); + Require(idempotentConfig.size == 24 && idempotentConfig.capacity == 4194304 && + idempotentConfig.setting == 3 && idempotentConfig.port == 4001, + "Default configuration must remain consistent across repeated calls"); + + for (const auto shift : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) { + for (const auto initial : std::array<std::uint64_t, 5>{0, 1, 24, 560, UINT64_MAX}) { + for (const unsigned char pattern : {0x00, 0x5a, 0xa5, 0xff}) { + std::array<unsigned char, 64> storage; + storage.fill(pattern); + auto* config = storage.data() + 16 + shift; + std::memcpy(config, &initial, sizeof(initial)); + errno = 7; + Require(sceSulphaGetDefaultConfig(config) == 0 && errno == 7, + "Default configuration must accept non-null storage regardless of its incoming size"); + Config result; + std::memcpy(&result, config, sizeof(result)); + Require(result.size == 24 && result.capacity == 4194304 && result.setting == 3 && result.port == 4001, + "Default configuration must match the native 24-byte record"); + for (std::size_t i = 0; i < storage.size(); ++i) { + if (i < 16 + shift || i >= 16 + shift + sizeof(Config)) + Require(storage[i] == pattern, "Default configuration must preserve bytes outside its record"); + } } } } + } From 3f14d7165b76a5722d3098e3770ad7b8c6d9d434 Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 18:16:00 +0100 Subject: [PATCH 565/702] test(libSceRazorCpu): cover null and zero boundary arguments in GuestRazorCpu Cover null string and zero count/size arguments across job manager, marker, plot, bookmark, logical file access, and data tag functions. --- core/libs/tests/GuestRazorCpu.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index f938b7a80b..63f449ac1d 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -15,14 +15,20 @@ void Require(bool value) { if (!value) std::abort(); } int main() { Require(sceRazorCpuIsCapturing() == 0); Require(sceRazorCpuJobManagerDispatch(nullptr, 1, 2, 3, 4, 5, 6) == 0); + Require(sceRazorCpuJobManagerDispatch("job", 0, 0, 0, 0, 0, 0) == 0); Require(sceRazorCpuJobManagerJob(1, 2, 3, 16) == 0); + Require(sceRazorCpuJobManagerJob(0, 0, 0, 0) == 0); Require(sceRazorCpuJobManagerSequence(nullptr, 1, 2, 3) == 0); + Require(sceRazorCpuJobManagerSequence("sequence", 0, 0, 0) == 0); Require(sceRazorCpuNamedSync("frame") == 0); + Require(sceRazorCpuNamedSync(nullptr) == 0); Require(sceRazorCpuPushMarkerStatic("outer", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPushMarkerStatic("inner", 0x80ffffffu, 2) == 0); + Require(sceRazorCpuPushMarkerStatic(nullptr, 0, 0) == 0); Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPushMarker("copied", 0xff00ff00u, 1) == 0); + Require(sceRazorCpuPushMarker(nullptr, 0, 0) == 0); Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPushMarkerStatic("unbalanced", 0x80ffffffu, 2) == 0); Require(sceRazorCpuIsCapturing() == 0); @@ -31,8 +37,12 @@ int main() { Require(timeSpentInFlush == 0x123456789abcdef0ull); Require(sceRazorCpuFlushOccurred(nullptr) == 0); Require(sceRazorCpuPlotValue("read time (ms)", 1.5f) == 0); + Require(sceRazorCpuPlotValue(nullptr, 0.0f) == 0); + Require(sceRazorCpuPlotValue("read time (ms)", -1.0f) == 0); Require(sceRazorCpuWriteBookmark("read timeout", "lba=0x10") == 0); Require(sceRazorCpuWriteBookmark("read timeout", nullptr) == 0); + Require(sceRazorCpuWriteBookmark(nullptr, nullptr) == 0); + Require(sceRazorCpuWriteBookmark(nullptr, "description") == 0); Require(sceRazorCpuSync() == 0); Require(sceRazorCpuSync() == 0); Require(sceRazorCpuIsCapturing() == 0); @@ -45,6 +55,7 @@ int main() { Require(sceRazorCpuBeginLogicalFileAccess("data/level.pak", 0x0123456789abcdefull, 4096, 0) == 0); Require(sceRazorCpuEndLogicalFileAccess() == 0); Require(sceRazorCpuBeginLogicalFileAccess("save/slot0.bin", 0, 512, 1) == 0); + Require(sceRazorCpuBeginLogicalFileAccess(nullptr, 0, 0, 0) == 0); Require(sceRazorCpuEndLogicalFileAccess() == 0); Require(sceRazorCpuEndLogicalFileAccess() == 0); @@ -53,11 +64,16 @@ int main() { int elements[16]{}; unsigned char buffer[64]{}; Require(sceRazorCpuInitDataTags(storage, sizeof(storage)) == 0); + Require(sceRazorCpuInitDataTags(nullptr, 0) == 0); Require(sceRazorCpuTagArray("elements", 7, elements, sizeof(elements), sizeof(elements[0])) == 0); + Require(sceRazorCpuTagArray(nullptr, 0, nullptr, 0, 0) == 0); Require(sceRazorCpuTagBuffer("buffer", 7, buffer, sizeof(buffer)) == 0); + Require(sceRazorCpuTagBuffer(nullptr, 0, nullptr, 0) == 0); Require(sceRazorCpuResizeTaggedBuffer(7, buffer, sizeof(buffer) / 2) == 0); + Require(sceRazorCpuResizeTaggedBuffer(0, nullptr, 0) == 0); Require(sceRazorCpuUnTagBuffer(7, buffer) == 0); Require(sceRazorCpuUnTagBuffer(7, elements) == 0); + Require(sceRazorCpuUnTagBuffer(0, nullptr) == 0); Require(sceRazorCpuShutdownDataTags() == 0); for (const unsigned char byte : storage) Require(byte == 0x5a); Require(sceRazorCpuTagBuffer("untracked", 3, buffer, sizeof(buffer)) == 0); From 5c80c032a2e81043b9451155e9725655e8287aab Mon Sep 17 00:00:00 2001 From: PrivacyOnion <granfili074@gmail.com> Date: Tue, 27 Oct 2026 18:17:06 +0100 Subject: [PATCH 566/702] test(libSceBluetoothHid): cover callback lifecycle and parameter validation in GuestBluetoothHid Cover non-null callback registration before init, post-unregister noCallback state on duplicate unregister and device registration, and null parameter initialization. --- core/libs/tests/GuestBluetoothHid.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestBluetoothHid.cpp b/core/libs/tests/GuestBluetoothHid.cpp index 56965517eb..718f2442c3 100644 --- a/core/libs/tests/GuestBluetoothHid.cpp +++ b/core/libs/tests/GuestBluetoothHid.cpp @@ -30,6 +30,7 @@ int main() { constexpr auto invalidArgument = static_cast<std::int32_t>(0x81540001u); constexpr auto noCallback = static_cast<std::int32_t>(0x81540006u); Require(sceBluetoothHidRegisterCallback(nullptr, nullptr, nullptr) == notInitialized, "initialization check order"); + Require(sceBluetoothHidRegisterCallback(&Callback, nullptr, nullptr) == notInitialized, "non-null callback before initialization"); Require(sceBluetoothHidRegisterDevice(1, 2) == notInitialized, "registration before initialization"); Require(sceBluetoothHidUnregisterDevice(1, 2) == notInitialized, "unregistration before initialization"); Require(sceBluetoothHidUnregisterCallback() == notInitialized, "callback removal before initialization"); @@ -95,6 +96,8 @@ int main() { Require(threw, "set feature report did not throw"); Require(sceBluetoothHidUnregisterDevice(0x44f, 0x1000) == 0, "device unregistration failed"); Require(sceBluetoothHidUnregisterCallback() == 0, "callback unregistration failed"); + Require(sceBluetoothHidUnregisterCallback() == noCallback, "duplicate callback unregistration"); + Require(sceBluetoothHidRegisterDevice(0x44f, 0x1000) == noCallback, "device registration after callback unregistration"); BluetoothHidThreadParam thread{}; BluetoothHidParam filled; std::memset(&filled, 0xa5, sizeof(filled)); @@ -111,6 +114,7 @@ int main() { std::memset(&filled, 0x5a, sizeof(filled)); Require(sceBluetoothHidParamInitialize(&filled, nullptr) == 0 && filled.threadParam == nullptr && filled.reserved == 0 && filled.padding == 0x5a5a5a5au, "a null thread parameter was not stored as given"); Require(sceBluetoothHidParamInitialize(nullptr, &thread) == invalidArgument, "a null parameter block was not refused"); + Require(sceBluetoothHidParamInitialize(nullptr, nullptr) == invalidArgument, "null parameter and null thread parameter were not refused"); std::puts("BluetoothHid tests passed"); return 0; } From 95a785104ca651d759c191c127637ec8268f43db Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Tue, 27 Oct 2026 13:27:52 -0500 Subject: [PATCH 567/702] fix(libkernel): treat a raise target that is already terminating as exited on Windows SuspendThread fails with STATUS_THREAD_IS_TERMINATING for a thread that has begun to exit while its handle is not signaled yet. TryDeliver took only the signaled handle as an exit, so the raise deliverer, which keeps sweeping a target in host code until delivery or exit, threw "cannot suspend the target thread" and aborted the process when that target exited in between. The suspension now reads the NTSTATUS from NtSuspendThread and treats a terminating target as exited; other failures still throw. guest_raise_exiting_thread raises at 32 threads per round that block in a host wait and exit after it, for 100 rounds. --- core/libs/prx/libkernel/CMakeLists.txt | 7 +++ .../prx/libkernel/System/src/Exception.cpp | 17 +++++- core/libs/tests/GuestRaiseExitingThread.cpp | 55 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestRaiseExitingThread.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 0ca40bf83c..e25803eb75 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1210,6 +1210,13 @@ if(BUILD_TESTING) add_test(NAME guest_raise_nested_sleep COMMAND guest_raise_nested_sleep_tests) set_tests_properties(guest_raise_nested_sleep PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") + add_test_executable(guest_raise_exiting_thread_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseExitingThread.cpp) + target_include_directories(guest_raise_exiting_thread_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_exiting_thread_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_raise_exiting_thread_tests) + add_test(NAME guest_raise_exiting_thread COMMAND guest_raise_exiting_thread_tests) + set_tests_properties(guest_raise_exiting_thread PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") endif() if(WIN32) add_library(guest_unwind_module_info_fixture SHARED ${LIBS_INCLUDE_DIR}/tests/GuestUnwindModuleInfoFixture.cpp) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index b0ec9a9df8..363606d882 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -319,6 +319,19 @@ bool Exited(HANDLE native) { return WaitForSingleObject(native, 0) == WAIT_OBJECT_0; } +using NtSuspendThreadFunction = LONG(NTAPI*)(HANDLE, PULONG); +constexpr LONG ThreadIsTerminating = static_cast<LONG>(0xc000004bu); + +LONG SuspendTarget(HANDLE native) { + static const auto suspend = [] { + const auto found = reinterpret_cast<NtSuspendThreadFunction>(reinterpret_cast<void*>(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtSuspendThread"))); + if (found == nullptr) throw std::runtime_error("sceKernelRaiseException: ntdll.dll has no NtSuspendThread"); + return found; + }(); + ULONG previous = 0; + return suspend(native, &previous); +} + bool RestoringContext(DWORD64 rip) { static const std::array<DWORD64, 2> stubs = [] { const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); @@ -695,7 +708,9 @@ Attempt TryDeliver(Pthread thread, HANDLE native, GuestExceptionHandler handler, alignas(16) Delivery delivery{handler, signum, {}, {}, SaveVectors ? 1u : 0u}; const auto contextDeadline = std::chrono::steady_clock::now() + RetryLimit; for (;;) { - if (SuspendThread(native) == static_cast<DWORD>(-1)) { + const LONG suspended = SuspendTarget(native); + if (suspended == ThreadIsTerminating) return Attempt::Exited; + if (suspended < 0) { if (Exited(native)) return Attempt::Exited; throw std::runtime_error("sceKernelRaiseException: cannot suspend the target thread"); } diff --git a/core/libs/tests/GuestRaiseExitingThread.cpp b/core/libs/tests/GuestRaiseExitingThread.cpp new file mode 100644 index 0000000000..0af6dcc35c --- /dev/null +++ b/core/libs/tests/GuestRaiseExitingThread.cpp @@ -0,0 +1,55 @@ +#include "prx/libkernel/include/Api.hpp" +#include "SceTypes.hpp" +#include <windows.h> +#include <array> +#include <atomic> +#include <cstdio> +#include <cstdlib> +#include <thread> + +static constexpr int SIGUSR1 = 30; +static constexpr int SCE_OK = 0; +static constexpr int Rounds = 100; +static constexpr std::size_t Threads = 32; + +static void Require(bool value) { if (!value) std::abort(); } + +static std::atomic<int> handled{0}; +static std::atomic<int> waiting{0}; +static HANDLE release = nullptr; + +static void APS5_VABI Handler(int signum, void*) { + Require(signum == SIGUSR1); + handled.fetch_add(1); +} + +static void* APS5_VABI Exiting(void*) { + waiting.fetch_add(1); + Require(WaitForSingleObject(release, INFINITE) == WAIT_OBJECT_0); + return nullptr; +} + +int main() { + release = CreateEventW(nullptr, TRUE, FALSE, nullptr); + Require(release != nullptr); + Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast<void*>(&Handler)) == 0); + int raised = 0; + for (int round = 0; round < Rounds; ++round) { + Require(ResetEvent(release) != 0); + waiting.store(0); + std::array<Pthread, Threads> threads{}; + for (auto& thread : threads) Require(scePthreadCreate(&thread, nullptr, Exiting, nullptr, "exiting") == SCE_OK); + while (waiting.load() != static_cast<int>(Threads)) std::this_thread::yield(); + for (const auto thread : threads) { + Require(sceKernelRaiseException(thread, SIGUSR1) == SCE_OK); + ++raised; + } + Require(SetEvent(release) != 0); + for (const auto thread : threads) Require(scePthreadJoin(thread, nullptr) == SCE_OK); + } + Require(handled.load() <= raised); + Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); + Require(CloseHandle(release) != 0); + std::printf("%d raises at threads that exit after their host wait, %d handled\n", raised, handled.load()); + return 0; +} From c7f22c2778c3928f7709c2dd2fbadb245ec0d2be Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 18:51:10 +0000 Subject: [PATCH 568/702] docs(libkernel): note how titles' libc reads the sanitizer replacement tables Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f6e4026722..207443fabb 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -12,6 +12,7 @@ - [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 - [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - internal pool allocation and statistics are not implemented, so a non-null query throws without changing its output. Native null output returns -1; an absent pool returns -2, a failed statistics query returns -3, and success writes total pool bytes minus used bytes. - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) - the native zero-argument query returns bit 3 of the process sanitizer flags loaded from sysctl `{1, 14, 41}`. The host always reports disabled because guest sanitizer activation is not modeled. +- [sceKernelGetSanitizerMallocReplaceExternal and sceKernelGetSanitizerNewReplaceExternal](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel) - return a static zeroed table whose first field holds its size (0x70 bytes for malloc, 0x68 for new). The `libc.prx` that nine of the ten local titles ship in `sce_module` takes a table only when the pointer and each function pointer it checks are non-null, so with these tables it keeps its own allocator, as it would with a null pointer. All nine read one more malloc entry, at 0x70, only when the size is at least 0x78, and the new entries from 0x50 only when its size is at least 0x70, so the sizes reported here leave them out. What the console returns when no sanitizer is loaded is unknown. - [pthread_cond_setname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) - takes `(cond, name)` and returns 0 without keeping the name, as `scePthreadCondInit` also ignores its name and nothing reads one back; a statically initialized condition variable is created first, and a destroyed one returns `EINVAL`. FreeBSD 11.4 libthr has no such function; the signature comes only from shadPS4 ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/kernel/threads/condvar.cpp#L80-L88)), which stores the name, returns `EINVAL` for a null `name`, and returns 0 for a statically initialized condition variable without creating it. A null `cond` or a null `name` throws here, because the console's result for either was not measured. ### Unknown function info From 708808bd35f1b3d911144f589b4054dbeaf985de Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 27 Oct 2026 15:06:29 -0400 Subject: [PATCH 569/702] fix(libkernel): report ELOOP from truncate on a looping Windows link sceKernelTruncate and truncate checked for a symbolic link loop only after the native truncate failed, but on Windows std::filesystem::status already reports a self-referencing link as missing, so the call returned ENOENT before reaching that check. The loop check now runs before the not-found result too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/File/src/Stdio.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 4d231a4fff..457bcaa7f4 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1716,7 +1716,12 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt std::error_code error; const auto status = std::filesystem::status(native, error); if (const int failure = PathLookupFailure(error)) return failure; - if (!std::filesystem::exists(status)) return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); + if (!std::filesystem::exists(status)) { +#ifdef _WIN32 + if (File::WindowsSymbolicLinkLoop(native)) return SceKernelError(GUEST_ELOOP); +#endif + return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); + } if (std::filesystem::is_directory(status)) return SceErrorFromErrno(GUEST_EISDIR); if (const int failure = NativeTruncate(native, length)) { #ifdef _WIN32 From 3a91f8ab8e15a3a168f58e1c7099a41c50fc53a9 Mon Sep 17 00:00:00 2001 From: legendary-cookie <sakzhenli04@outlook.com> Date: Wed, 28 Oct 2026 01:46:32 +0800 Subject: [PATCH 570/702] fix(hw-oracle): read the template and write the assembly source as UTF-8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- tools/hw-oracle/hw_oracle.py | 4 ++-- tools/hw-oracle/test_hw_oracle.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index f1dc2da856..437b8945ee 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -111,13 +111,13 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 raise ValueError(f"{name} must be an integer from 0 to {limit - 1}") if not isinstance(lds, int) or lds not in range(MAX_LDS + 1): raise ValueError(f"lds must be an integer from 0 to {MAX_LDS}") - text = (HERE / "template.s").read_text() + text = (HERE / "template.s").read_text(encoding="utf-8") for key, value in (("@TARGET@", target()), ("@WAVE32@", "0" if wave64 else "1"), ("@WAVESIZE@", "64" if wave64 else "32"), ("@DENORM@", str(int(denorm32))), ("@DENORM16@", str(int(denorm16))), ("@IEEE@", str(int(ieee))), ("@DX10_CLAMP@", str(int(dx10_clamp))), ("@ROUND32@", str(int(round32))), ("@ROUND16@", str(int(round16))), ("@FP16_OVERFLOW@", str(int(fp16_overflow))), ("@LDS@", str(lds)), ("@BODY@", body)): text = text.replace(key, value) - (work / "k.s").write_text(text) + (work / "k.s").write_text(text, encoding="utf-8") if runtime() == "hip": (work / "k.co").write_bytes(hip_runtime().assemble(text.encode("utf-8"), target(), wave64)) return work / "k.co" diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 32f8dbd37f..7d9d5d0147 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -626,6 +626,17 @@ class GroupSegmentTests(unittest.TestCase): self.assertNotIn("@LDS@", text) +class SourceEncodingTests(unittest.TestCase): + def test_assembly_source_is_utf8(self): + body = " s_nop 0 ; v0 \u2192 v10, caf\u00e9" + with tempfile.TemporaryDirectory() as tmp, patch.object(hw_oracle, "target", return_value="gfx1036"), \ + patch.object(hw_oracle, "runtime", return_value="hsa"), patch.object(hw_oracle.subprocess, "run"), \ + patch.object(hw_oracle, "tool", return_value="clang"): + hw_oracle.assemble(body, Path(tmp), False, **MODES) + text = (Path(tmp) / "k.s").read_bytes().decode("utf-8") + self.assertIn(body, text) + + class AssemblyTests(unittest.TestCase): def test_condition_operands_match_wave_size(self): for name in ("clang", "ld.lld"): From 6135955c523c5745e2bf00c3b659f284c72d07e4 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 27 Oct 2026 15:21:51 -0400 Subject: [PATCH 571/702] test(libkernel_sys): remove the test's symbolic links with guest unlink With symbolic links allowed on Windows, guest_kernel_sys failed at its cleanup: MinGW's std::filesystem::remove reads links through stat, so a dangling link looks absent and is left in place, and the check expecting true fails. The two links are now removed with unlink_nid_postfix, which also checks that guest unlink removes a live and a dangling link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestKernelSys.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 70d7a04666..5146b2cae2 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -151,7 +151,7 @@ void OwnershipAndLinkTests(const std::filesystem::path& root) { Require(!std::filesystem::is_symlink(std::filesystem::symlink_status(other))); #endif Require(std::filesystem::hard_link_count(file) == 3); - Require(std::filesystem::remove(other) && std::filesystem::remove(symbolic) && std::filesystem::remove(dangling)); + Require(std::filesystem::remove(other) && unlink_nid_postfix(symbolic.c_str()) == 0 && unlink_nid_postfix(dangling.c_str()) == 0); } Require(sceKernelClose(directory) == 0); Require(std::filesystem::remove(linked) && std::filesystem::remove(file)); From 625a3fbb62a6e132cc7e00c8e13f6bf27bad6fab Mon Sep 17 00:00:00 2001 From: KernelPanicNT <invoice@juegatujuego.com> Date: Tue, 27 Oct 2026 19:24:23 +0000 Subject: [PATCH 572/702] fix(libkernel): store any scePthreadMutexattrSetkind kind as firmware 13.42 does scePthreadMutexattrSetkind called NotImplemented_nid_no_patch for a kind outside 1 to 4. Firmware 13.42's libkernel, run in an emulator, makes it call pthread_mutexattr_setkind_np, which stores any kind as a signed 16-bit value and returns 0, and turns a null attribute's -1 with errno 22 into 0x8001FFFF. It now forwards to pthread_mutexattr_setkind_np and converts the result with sceKernelError, so a null attribute or object still returns 0x8001FFFF and now also sets errno 22. An out-of-range kind is refused by scePthreadMutexInit with 0x80020016, leaving the mutex untouched, as measured for the same module. scePthreadMutexattrGetkind is unchanged; kind 0 now reaches it and returns 0. --- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 7 +--- core/libs/tests/GuestKernelSysPthreadNp.cpp | 33 +++++++++++-------- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index cc996fbd8c..3c3b7b8b89 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -5,7 +5,6 @@ #include "../include/Mutex.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" -#include "prx/libc/include/General.hpp" #include <algorithm> #include <atomic> #include <chrono> @@ -13,7 +12,6 @@ #include <limits> #include <memory> #include <stdexcept> -#include <string> namespace { @@ -144,10 +142,7 @@ int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr) { } int APS5_VABI scePthreadMutexattrSetkind(PthreadMutexattr* attr, int kind) { - if (!attr || !*attr) return sceKernelError(posixFailure); - if (kind < static_cast<int>(MutexType::ErrorCheck) || kind > static_cast<int>(MutexType::Adaptive)) - NotImplemented_nid_no_patch("scePthreadMutexattrSetkind with a kind other than 1 to 4"); - return scePthreadMutexattrSettype(attr, kind); + return sceKernelError(pthread_mutexattr_setkind_np_nid_postfix(attr, kind)); } int APS5_VABI scePthreadMutexattrGetkind(PthreadMutexattr attr) { diff --git a/core/libs/tests/GuestKernelSysPthreadNp.cpp b/core/libs/tests/GuestKernelSysPthreadNp.cpp index a1cd8ca017..29f905679f 100644 --- a/core/libs/tests/GuestKernelSysPthreadNp.cpp +++ b/core/libs/tests/GuestKernelSysPthreadNp.cpp @@ -1,17 +1,20 @@ +#include "prx/libc/include/Api.hpp" #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Mutex.hpp" #include "prx/libkernel/Pthread/include/Rwlock.hpp" #include "SceTypes.hpp" #include <cstdio> #include <exception> #include <initializer_list> -#include <stdexcept> #include <string_view> +#include <utility> namespace { constexpr int GUEST_EDEADLK = 11; constexpr int GUEST_EINVAL = 22; constexpr int SCE_FAILED_CALL = static_cast<int>(0x8001ffffu); +constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016u); constexpr unsigned SCE_KERNEL_ERROR_BASE = 0x80020000u; constexpr int KIND_ERRORCHECK = 1; constexpr int KIND_RECURSIVE = 2; @@ -31,16 +34,6 @@ void Case(const char* name, TCall call) { ++failures; } -template<typename TException, typename TCall> -bool Throws(TCall call) { - try { - static_cast<void>(call()); - } catch (const TException&) { - return true; - } - return false; -} - bool HasKind(const PthreadMutexattr* attr, int kind) { int type = -1; return pthread_mutexattr_gettype_nid_postfix(attr, &type) == 0 && type == kind; @@ -63,11 +56,20 @@ void TestKind() { } if (!LocksTwice(&attr, GUEST_EDEADLK)) return false; if (scePthreadMutexattrSetkind(&attr, KIND_RECURSIVE) != 0 || !LocksTwice(&attr, 0)) return false; - for (const int kind : {0, 5, -1}) { - if (!Throws<std::runtime_error>([&] { return scePthreadMutexattrSetkind(&attr, kind); }) || !HasKind(&attr, KIND_RECURSIVE)) return false; + for (const auto [kind, stored] : {std::pair{0, 0}, std::pair{5, 5}, std::pair{-1, -1}, std::pair{0x8000, -32768}, std::pair{0x12345, 0x2345}}) { + if (scePthreadMutexattrSetkind(&attr, kind) != 0 || pthread_mutexattr_getkind_np_nid_postfix(attr) != stored) return false; + PthreadMutex mutex = nullptr; + if (scePthreadMutexInit(&mutex, &attr, nullptr) != SCE_KERNEL_ERROR_EINVAL || mutex != nullptr) return false; } + if (scePthreadMutexattrSetkind(&attr, 0x10001) != 0 || !HasKind(&attr, KIND_ERRORCHECK) || !LocksTwice(&attr, GUEST_EDEADLK)) return false; if (pthread_mutexattr_destroy_nid_postfix(&attr) != 0) return false; - return scePthreadMutexattrSetkind(&attr, KIND_NORMAL) == SCE_FAILED_CALL && scePthreadMutexattrSetkind(nullptr, KIND_NORMAL) == SCE_FAILED_CALL; + for (PthreadMutexattr* target : {&attr, static_cast<PthreadMutexattr*>(nullptr)}) { + for (const int kind : {KIND_NORMAL, 5}) { + *__error_nid_postfix() = 0; + if (scePthreadMutexattrSetkind(target, kind) != SCE_FAILED_CALL || *__error_nid_postfix() != GUEST_EINVAL) return false; + } + } + return true; }); Case("scePthreadMutexattrGetkind", [] { PthreadMutexattr attr = nullptr; @@ -75,6 +77,9 @@ void TestKind() { for (const int kind : {KIND_RECURSIVE, KIND_NORMAL, KIND_ADAPTIVE, KIND_ERRORCHECK}) { if (scePthreadMutexattrSetkind(&attr, kind) != 0 || scePthreadMutexattrGetkind(attr) != static_cast<int>(SCE_KERNEL_ERROR_BASE + static_cast<unsigned>(kind))) return false; } + for (const auto [kind, result] : {std::pair{0, 0}, std::pair{5, static_cast<int>(SCE_KERNEL_ERROR_BASE + 5u)}, std::pair{0x12345, static_cast<int>(SCE_KERNEL_ERROR_BASE + 0x2345u)}}) { + if (scePthreadMutexattrSetkind(&attr, kind) != 0 || scePthreadMutexattrGetkind(attr) != result) return false; + } if (pthread_mutexattr_destroy_nid_postfix(&attr) != 0) return false; return scePthreadMutexattrGetkind(nullptr) == SCE_FAILED_CALL; }); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 78a6c47629..3c89906007 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -17,7 +17,7 @@ ### Unknown function info - [sceCoredumpRegisterCoredumpHandler, sceCoredumpUnregisterCoredumpHandler](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) retain registration metadata but do not create the native report thread or deliver callbacks. Native registration-state precedence, stack bounds and SDK capture are recovered; thread-creation failures and active-report synchronization remain unimplemented. -- [scePthreadMutexattrSetkind, scePthreadMutexattrGetkind](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - `scePthreadMutexattrSetkind` throws `NotImplemented_nid_no_patch` for a kind other than `MutexType::ErrorCheck` (1) to `MutexType::Adaptive` (4): the firmware stores any kind as 16 bits of the type field unchecked, but what a mutex does with such a kind is not measured. Kinds 1 to 4 are set through `scePthreadMutexattrSettype`, and a null attribute or object returns 0x8001FFFF. `scePthreadMutexattrGetkind` takes the attribute itself and returns 0x80020000 plus its kind, or 0x8001FFFF for a null one. The firmware returns 0 for kind 0, which no setter here stores, so that case is untested. Both follow [BunnyFl's measurements](https://github.com/boykopovar/AnyPS5/pull/9632#issuecomment-6363402814) of firmware 13.42's libkernel +- [scePthreadMutexattrSetkind, scePthreadMutexattrGetkind](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - not measured on a console or in a title process, only by running firmware 13.42's `libkernel.sprx` and `libkernel_sys.sprx` in an emulator ([#9632](https://github.com/boykopovar/AnyPS5/pull/9632#issuecomment-6363402814), [#10725](https://github.com/boykopovar/AnyPS5/pull/10725#issuecomment-6397556344)). `scePthreadMutexattrGetkind` of a negative stored kind returns 0x80020000 plus the kind, which follows the measured rule for other kinds but was not measured. `scePthreadMutexattrSetkind` stores any kind through `pthread_mutexattr_setkind_np`; for a kind outside 1 to 4 only the refusal by `scePthreadMutexInit` and `pthread_mutex_init` was measured - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw From 95e7e78e0e0033e4fa4c38c02e27464979d3910c Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 27 Oct 2026 15:24:53 -0400 Subject: [PATCH 573/702] test(libkernel): unlink the timestamp test's symbolic link before cleanup With symbolic links allowed on Windows, guest_file_timestamps hung until the CTest timeout: its link.txt is dangling (the relative target resolves beside the link), MinGW's std::filesystem::remove_all reads links through stat, sees it as absent and spins on a directory it cannot empty. The test now removes the link with unlink_nid_postfix first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestFileTimestamps.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 8a081181a5..03965aacfd 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -255,6 +255,7 @@ int main() { FileStat linkStatus{}; Require(lstat_nid_postfix(linkPath.c_str(), &linkStatus) == 0 && Same(linkStatus.st_mtim, first[1]), "symlink mtime updated"); #endif + Require(unlink_nid_postfix(linkPath.c_str()) == 0, "remove the symlink"); } int pipe[2]; Require(pipe_nid_postfix(pipe) == 0, "create pipe"); From e7ab802c957d8b68b14c3233c206388ac2814ddf Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:05:07 +0000 Subject: [PATCH 574/702] docs: correct the trace event buffer sizes in the build guide TraceThread::Slice holds five eight-byte members (scope, stage, begin, length and the recorder batch), so an event takes 40 bytes, not 32. The example sizes follow: 10 MiB at the default 262,144 events, 120 MiB at 3,145,728 and 160 MiB at the 4,194,304 maximum. --- docs/dev/BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 1154ea812c..340a891faf 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -259,7 +259,7 @@ Set `APS5_SAMPLE_WORKER=<file>` to sample queue worker 0 every millisecond (in p Set `APS5_TRACE_FRAMES=<start>:<count>` to record `<count>` presents from present `<start>` (counted from 0), or `APS5_TRACE_SECONDS=<start>:<length>` to record from the first present at least `<start>` seconds after the first present until the first present `<length>` seconds later. Both values are whole numbers and the length is above 0; a malformed value, or both switches together, fails the first present. The window opens and closes at presents. -Set `APS5_TRACE_EVENTS=<count>` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 32 bytes per event): 8 MiB at the default, 96 MiB at `3145728`, and 128 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, as does a title thread named `title` that binds one the first time it registers a shader or resolves a shader ABI. The GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. +Set `APS5_TRACE_EVENTS=<count>` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 40 bytes per event, since each slice also carries its recorder batch): 10 MiB at the default, 120 MiB at `3145728`, and 160 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, as does a title thread named `title` that binds one the first time it registers a shader or resolves a shader ABI. The GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter `<thread> folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace-<commit>-<pid>.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. From cb7a97d4d44b18407739dd0e0654aede25bfd47b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:05:07 +0000 Subject: [PATCH 575/702] docs: say how compute pipeline capture reacts to its limits The directory limits (128 records, 256 MiB) throw CaptureLimit, which WriteOptional catches: capture disables itself with one stderr line and the pipeline is still created. Only the per-record 36 MiB limit throws out of the capture, so the old sentence described both as an error. --- docs/dev/BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 340a891faf..9dcc26f921 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -197,7 +197,7 @@ On Windows MinGW frees `thread_local` objects before their destructors run, so t ## Compute pipeline replay -Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-<identity>/capture.aps5cp`; identical records are deduplicated. The pipelines of the start-up f32 denormal flush probe are not captured. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. +Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-<identity>/capture.aps5cp`; identical records are deduplicated. The pipelines of the start-up f32 denormal flush probe are not captured. At 128 records or 256 MiB in total, capture turns itself off for the rest of the run after a `[compute-capture] optional capture disabled` line and the title keeps running; a single record over 36 MiB (at most 8,388,608 SPIR-V words, or 32 MiB of shader code) is an error. Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. Set `APS5_DUMP_COMPUTE_MIN_WORDS` to capture only modules at or above that SPIR-V word count, for example `100000` when investigating large NVIDIA compilations. The default is `0` (all modules); values outside `0` through `8388608` fail. The filter applies only when capture is enabled. From 846c386cd800373a247491272ccb26d96b7ec156 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:05:07 +0000 Subject: [PATCH 576/702] docs: give the zero padding of the submission dump file names SubmissionCapture formats the file name with %06llu, as the PM4 replay tests expect (submission_000006.bin). --- docs/dev/BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 9dcc26f921..e166e632be 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -215,7 +215,7 @@ Each invocation creates one pipeline. With no `--cache-in`, its application pipe ## PM4 submission replay -Set `APS5_DUMP_SUBMITS` to a directory before starting a title to save every submission the AGC driver accepts, as it enters the driver. `index.txt` gets one line per submission: ordinal, queue, `graphics` or `compute`, serial, microseconds on the clock of the `APS5_TRACE_GPU` lines, dword count, kind, file name, the dword offsets of the waits whose value passed their compare when the submission entered the driver (`held`, which the driver then does not wait for) and the COND_EXEC and predicated indirect buffer ranges as `start:end` dword offsets (`-` for none). `submission_<ordinal>.bin` holds the dwords the driver copied, with indirect buffers inlined. A REWIND tail is listed as `rewind` with the serial of its submission, a suspend point as `suspend` without a file. The index is truncated on the first submission. A failed write puts the driver in its failed state: the call that made the submission throws after the submission was queued, and every later driver call throws the same error. Unset, the switch costs one test per submission; set, one copy of the commands per submission before the driver lock, the ordinal, time and held waits under it without allocating, and the files written after it. A capture holds command buffers only, no guest memory. An index written before the held and conditional columns (column line `ordinal queue type serial time_us dwords kind file`) is still read, with both unknown. +Set `APS5_DUMP_SUBMITS` to a directory before starting a title to save every submission the AGC driver accepts, as it enters the driver. `index.txt` gets one line per submission: ordinal, queue, `graphics` or `compute`, serial, microseconds on the clock of the `APS5_TRACE_GPU` lines, dword count, kind, file name, the dword offsets of the waits whose value passed their compare when the submission entered the driver (`held`, which the driver then does not wait for) and the COND_EXEC and predicated indirect buffer ranges as `start:end` dword offsets (`-` for none). `submission_<ordinal>.bin`, with the ordinal zero-padded to six digits (`submission_000001.bin`), holds the dwords the driver copied, with indirect buffers inlined. A REWIND tail is listed as `rewind` with the serial of its submission, a suspend point as `suspend` without a file. The index is truncated on the first submission. A failed write puts the driver in its failed state: the call that made the submission throws after the submission was queued, and every later driver call throws the same error. Unset, the switch costs one test per submission; set, one copy of the commands per submission before the driver lock, the ordinal, time and held waits under it without allocating, and the files written after it. A capture holds command buffers only, no guest memory. An index written before the held and conditional columns (column line `ordinal queue type serial time_us dwords kind file`) is still read, with both unknown. `agc_pm4_replay <directory> [--full]` decodes the capture offline with the driver's packet decoder, validator and store decode (`Pm4::DecodeStores`) and reports each WAIT_REG_MEM, WAIT_REG_MEM_64 and REWIND as released, unreleased or unknown. It does not model cross-queue timing: a wait is released only when the capture alone proves that the driver releases it, and unknown, with the reason, whenever its outcome depends on when the stores of another queue run, since a store of another queue can run before, between or after the stores of the waiting queue whatever order the submissions entered the driver in. A memory wait is released when its compare always passes; when it was held at submit; when the last store to the awaited bytes before it on its queue (in that submission or an earlier one) covers them with a value the compare accepts, is not conditional, and no other queue stores to any of those bytes anywhere in the capture (current value); or when a RELEASE_MEM or WRITE_DATA label earlier in the waiting submission, one the driver keeps in its label history (4-byte aligned, at most 64 bytes, not conditional), stores a value the compare accepts and fewer than 4 other stores to the awaited bytes can run between it and the wait (label history). The driver keeps the last 4 label stores per dword over all queues (`LabelHistory.cpp`) and finds one there only if it ran after the waiting submission entered the driver, which the capture proves only for a store earlier in that submission. The stores counted against the history are those between the label and the wait on the waiting queue and every store of another queue to the awaited bytes, whatever its submit order; stores later on the waiting queue run after the wait. A REWIND is released when its tail is in the capture. One that was valid at submit without a tail in the capture is unknown, because the driver reads the valid bit again when the queue reaches the REWIND. A memory wait is unreleased when it was not held at submit and no store in the capture that can run before it may write a value the compare accepts (a store of part of the awaited bytes or of a value known only at run time may): the GPU does not release it, but a CPU store could, and so could a shader store, since the capture holds neither. A REWIND without a tail that was not valid at submit is unreleased. Every other wait is unknown: `unknown: depends on cross-queue timing (stores on queue X at #...)` when another queue stores to the awaited bytes; otherwise when the last store before it on its queue is conditional (COND_WRITE, inside a COND_EXEC range or a predicated packet), stores a value known only at run time or only part of the awaited bytes, or overwrites an accepted store that the capture does not prove is still in the label history (one in an earlier submission, one the history does not keep, or one with 4 or more stores after it). When the index has no held column, a wait that would be unreleased is unknown, since its value may have passed the compare at submit; without the conditional column every packet after a COND_EXEC or predicated INDIRECT_BUFFER is conditional to the end of its submission. The report counts the waits per outcome and route and the unknown waits that depend on cross-queue timing, says when the index lacks the columns, lists up to 32 unreleased and then unknown waits, newest first, each with the packets around it and the stores to the awaited bytes that decide it (for another queue, the 4 nearest the waiting submission in submit order), then the last submissions of each queue. `--full` lists every wait and prints every packet. A packet the driver rejects means the capture is corrupt and fails the analysis. The exit code is 1 when a wait is unreleased and 2 on errors. From ddc4aad95aa3f86f30b41bc3e97e6d258b2a18da Mon Sep 17 00:00:00 2001 From: Mohamed Malek Aifa <32399935+malekaifa@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:06:17 +0100 Subject: [PATCH 577/702] fix(tools): validate CMake file API reply objects --- tools/build_mingw_host_only_tests.py | 42 +++++++++++-- .../test_mingw_host_only_test_targets.py | 63 +++++++++++++++++++ 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/tools/build_mingw_host_only_tests.py b/tools/build_mingw_host_only_tests.py index 15b58d7eda..4f530ed8e2 100644 --- a/tools/build_mingw_host_only_tests.py +++ b/tools/build_mingw_host_only_tests.py @@ -6,23 +6,55 @@ import subprocess import sys +def object_document(value, path, name): + if not isinstance(value, dict): + raise ValueError(f"{path}: invalid {name} (expected an object)") + return value + + +def object_entries(value, path, name): + if not isinstance(value, list) or any(not isinstance(item, dict) for item in value): + raise ValueError(f"{path}: invalid {name} (expected a list of objects)") + return value + + +def target_document(value, path): + target = object_document(value, path, "CMake target") + if "name" in target and not isinstance(target["name"], str): + raise ValueError(f"{path}: invalid CMake target name (expected a string)") + link = object_document(target.get("link", {}), path, "CMake target link") + object_entries(link.get("commandFragments", []), path, "CMake link command fragments") + object_entries(target.get("artifacts", []), path, "CMake target artifacts") + object_entries(target.get("sources", []), path, "CMake target sources") + return target + + def read_reply(build_dir): reply_dir = build_dir / ".cmake" / "api" / "v1" / "reply" indexes = list(reply_dir.glob("index-*.json")) if not indexes: raise ValueError(f"{reply_dir}: no CMake File API index; configure with the codemodel-v2 query first") index = max(indexes, key=lambda path: path.stat().st_mtime_ns) - document = json.loads(index.read_text(encoding="utf-8")) - codemodel = document.get("reply", {}).get("codemodel-v2") + document = object_document(json.loads(index.read_text(encoding="utf-8")), index, "CMake File API index") + reply = object_document(document.get("reply", {}), index, "CMake File API reply") + codemodel = reply.get("codemodel-v2") + if codemodel is None: + raise ValueError(f"{index}: missing codemodel-v2 reply") + codemodel = object_document(codemodel, index, "CMake codemodel reference") if not codemodel: raise ValueError(f"{index}: missing codemodel-v2 reply") - model = json.loads((reply_dir / codemodel["jsonFile"]).read_text(encoding="utf-8")) + model_path = reply_dir / codemodel["jsonFile"] + model = object_document(json.loads(model_path.read_text(encoding="utf-8")), model_path, "CMake codemodel") configurations = model.get("configurations", []) if len(configurations) != 1: raise ValueError(f"{index}: expected one CMake configuration, found {len(configurations)}") + configuration = object_document(configurations[0], model_path, "CMake configuration") targets = {} - for target in configurations[0].get("targets", []): - targets[target["id"]] = json.loads((reply_dir / target["jsonFile"]).read_text(encoding="utf-8")) + for target in configuration.get("targets", []): + target = object_document(target, model_path, "CMake target reference") + target_path = reply_dir / target["jsonFile"] + targets[target["id"]] = target_document( + json.loads(target_path.read_text(encoding="utf-8")), target_path) return Path(model["paths"]["source"]).resolve(), targets diff --git a/tools/tests/test_mingw_host_only_test_targets.py b/tools/tests/test_mingw_host_only_test_targets.py index f1927be0fa..0b104711e7 100644 --- a/tools/tests/test_mingw_host_only_test_targets.py +++ b/tools/tests/test_mingw_host_only_test_targets.py @@ -1,3 +1,5 @@ +import contextlib +import io import json from pathlib import Path import sys @@ -157,6 +159,67 @@ class HostOnlyTargets(unittest.TestCase): with self.assertRaisesRegex(ValueError, "no CMake File API index"): build_mingw_host_only_tests.read_reply(Path(directory)) + def test_wrong_shaped_file_api_documents_are_reported_cleanly(self): + cases = ( + ("index", [], "CMake File API index"), + ("index", {"reply": []}, "CMake File API reply"), + ("index", {"reply": {"codemodel-v2": []}}, "CMake codemodel reference"), + ("model", [], "CMake codemodel"), + ("model", {"configurations": [[]], "paths": {"source": "."}}, "CMake configuration"), + ("model", {"configurations": [{"targets": [[]]}], "paths": {"source": "."}}, "CMake target reference"), + ("target", [], "CMake target"), + ) + for location, document, expected in cases: + with self.subTest(location=location, expected=expected), tempfile.TemporaryDirectory() as directory: + root = Path(directory) + reply = root / "build" / ".cmake" / "api" / "v1" / "reply" + reply.mkdir(parents=True) + target = self.target("host_tests") + target_name = "target-host_tests.json" + (reply / target_name).write_text(json.dumps(target), encoding="utf-8") + model = {"configurations": [{"targets": [{"id": target["id"], "jsonFile": target_name}]}], + "paths": {"source": str(root)}} + (reply / "codemodel.json").write_text(json.dumps(model), encoding="utf-8") + index = {"reply": {"codemodel-v2": {"jsonFile": "codemodel.json"}}} + (reply / "index-0001.json").write_text(json.dumps(index), encoding="utf-8") + path = {"index": reply / "index-0001.json", "model": reply / "codemodel.json", + "target": reply / target_name}[location] + path.write_text(json.dumps(document), encoding="utf-8") + with self.assertRaisesRegex(ValueError, expected): + build_mingw_host_only_tests.read_reply(root / "build") + errors = io.StringIO() + with contextlib.redirect_stderr(errors): + self.assertEqual(build_mingw_host_only_tests.main([str(root / "build"), "--list-only"]), 1) + self.assertIn(expected, errors.getvalue()) + self.assertNotIn("Traceback", errors.getvalue()) + + def test_wrong_shaped_target_members_are_reported_cleanly(self): + cases = ( + ({"name": []}, "CMake target name"), + ({"link": []}, "CMake target link"), + ({"link": {"commandFragments": ["bad"]}}, "CMake link command fragments"), + ({"artifacts": ["bad"]}, "CMake target artifacts"), + ({"sources": ["bad"]}, "CMake target sources"), + ) + for changes, expected in cases: + with self.subTest(expected=expected), tempfile.TemporaryDirectory() as directory: + root = Path(directory) + reply = root / "build" / ".cmake" / "api" / "v1" / "reply" + reply.mkdir(parents=True) + target = self.target("host_tests") + target.update(changes) + (reply / "target.json").write_text(json.dumps(target), encoding="utf-8") + model = {"configurations": [{"targets": [{"id": "host_tests", "jsonFile": "target.json"}]}], + "paths": {"source": str(root)}} + (reply / "codemodel.json").write_text(json.dumps(model), encoding="utf-8") + index = {"reply": {"codemodel-v2": {"jsonFile": "codemodel.json"}}} + (reply / "index-0001.json").write_text(json.dumps(index), encoding="utf-8") + errors = io.StringIO() + with contextlib.redirect_stderr(errors): + self.assertEqual(build_mingw_host_only_tests.main([str(root / "build"), "--list-only"]), 1) + self.assertIn(expected, errors.getvalue()) + self.assertNotIn("Traceback", errors.getvalue()) + if __name__ == "__main__": unittest.main() From 33577c6eab4f0046d678bf8675f3ebbcaefc738c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:14:32 +0000 Subject: [PATCH 578/702] docs: describe the macOS guest entry that Aps5StartGuest provides The table said Aps5StartGuest had no implementation on main, but MainThreadMacOs.cpp defines it and libkernel's CMakeLists builds it. It starts the guest on a second pthread (at least 8 MiB of stack) as entry(block, nullptr) while the main thread runs the CFRunLoop, and it aborts if the entry returns. --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index e63cb8cf20..39002eb822 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -53,7 +53,7 @@ The host stubs that transfer control to the guest `e_entry` are not the same on |---|---|---|---|---|---|---| | Linux | [`EntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp) | process stack: qword `argc`, then argv pointers, then a null | 0 | left as the host dynamic linker left it (often `rtld_fini`) | zero frame `{0, 0}` | `ud2` | | Windows | [`WindowsEntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) | `VirtualAlloc` block: qword `argc`, UTF-8 argv pointers, null | address of an exit trampoline that finalizes guest modules and returns | not set by the stub (last host call is `FreeLibrary`) | zero frame in the stub's stack | `ExitProcess` with the entry's return code | -| macOS | [`MacOsImage.cpp`](../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) | guest entry address into `_Aps5StartGuest_nid_no_patch` | stack block `{argc, argv…, 0, envp…, 0}` | — | — | `Aps5StartGuest` has no implementation on `main`, so the final guest-entry registers are unset | +| macOS | [`MacOsImage.cpp`](../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) | guest entry address into `_Aps5StartGuest_nid_no_patch` | stack block `{argc, argv…, 0, envp…, 0}` | — | — | [`Aps5StartGuest`](../../core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp) runs the guest entry as `entry(block, nullptr)` on a new thread with at least an 8 MiB stack (the main thread keeps serving its run loop); a return from the entry prints `guest entry returned` and aborts | On Linux the argument block is the process stack itself (`mov rdi, rsp`). On Windows it is a separate allocation the stub fills with UTF-8 argument strings. Titles that read only `argc`/`argv` through `rdi` see the same layout shape on both; titles that call through `rsi` on Windows get a host exit path that Linux does not provide (`rsi` is zero there). From 911024bd13d04e4742655b3c19154deb18cafb47 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:14:41 +0000 Subject: [PATCH 579/702] docs: link the cancellation points to the libkernel debt file TechnicalDebt.md only links back to this section; the list of calls that act on a cancellation request and the note on the asynchronous type are the Thread cancellation entry of debt/libkernel.md. --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 39002eb822..0f3ce51164 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -90,7 +90,7 @@ On Windows, a relinked game run with `APS5_MIMALLOC` set (to any value) allocate Cancellation exits through `scePthreadExit` with result pointer `1`. It runs cleanup handlers in reverse push order before thread-specific destructors; cancellation points inside cleanup must not start another exit. The host thread then terminates through the existing Windows or Linux lifecycle, and join waits for that termination. Host thread termination is not used to inject cancellation into a running guest frame. -The reference contract and constants come from FreeBSD libthr, pinned at [`5ed7eb0d`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_cancel.c), with cleanup and exit ordering in [`thr_exit.c`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_exit.c#L196-L259). These are reference semantics, not console measurements. The supported cancellation points and incomplete asynchronous behavior are listed in [TechnicalDebt](TechnicalDebt.md). +The reference contract and constants come from FreeBSD libthr, pinned at [`5ed7eb0d`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_cancel.c), with cleanup and exit ordering in [`thr_exit.c`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_exit.c#L196-L259). These are reference semantics, not console measurements. The supported cancellation points and incomplete asynchronous behavior are listed in the [libkernel technical debt](debt/libkernel.md). [`GuestPthreadCancelRecovery.cpp`](../../core/libs/tests/GuestPthreadCancelRecovery.cpp) checks pending requests before ordinary and timed semaphore acquisition, cleanup reentrancy and destructor ordering, enabled and disabled timed condition waits, canceled join output, and subsequent resource and thread reuse. Host atomic gates establish pending requests; acquiring the condition mutex after the worker releases it establishes wait entry without a scheduling sleep. Both `guest_pthread_cancel_recovery` and `guest_pthread_cancel_recovery_coarse` retain checks in Release and have a 20-second timeout, shorter than the 60-second guest waits. The latter sets `APS5_COARSE_TIMED_WAITS=1` to exercise the Windows fallback; Linux uses that condition-variable path by default. From f5947c66cbcbd4a188a58449603c114a6253a8bc Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:14:41 +0000 Subject: [PATCH 580/702] docs: say when an APR submit returns before its buffer has run _submit executes inline only when the queue is neither running nor holding pending work; otherwise it enqueues a copy and returns the id, so the statement that buffers without a wait always complete first was wrong. --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 0f3ce51164..216092711b 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -96,7 +96,7 @@ The reference contract and constants come from FreeBSD libthr, pinned at [`5ed7e ## APR and AMM command buffers -[`Apr.cpp`](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) executes APR and AMM command buffers. A buffer runs on the submitting thread until a `WaitOnAddress` or `WaitOnCounter` that is not yet satisfied; the rest of it, and every buffer submitted after it, is copied and runs on a worker thread, so a submitter that satisfies the wait after the submit returns does not hang. Buffers without such a wait still complete before the submit returns. +[`Apr.cpp`](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) executes APR and AMM command buffers. A buffer runs on the submitting thread until a `WaitOnAddress` or `WaitOnCounter` that is not yet satisfied; the rest of it, and every buffer submitted after it, is copied and runs on a worker thread, so a submitter that satisfies the wait after the submit returns does not hang. A buffer without such a wait completes before the submit returns only when the queue is idle; while an earlier buffer is running or still pending, it is copied behind it and the submit returns its id before it has run (`sceKernelAprWaitCommandBuffer` waits for it). APR and AMM each have their own queue and worker thread. Order is kept inside a queue; a wait in one does not hold up the other, because AMM `WriteAddress` commands advance the counters that APR buffers wait on. Submission ids are per queue, and `sceKernelAprWaitCommandBuffer` and `sceAmprAmmWaitCommandBufferCompletion` block until the queue has completed that id. A command that throws on a worker aborts the process. From 534f5acdc8e632ca36c2f11b398af2cf79e2aee0 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 581/702] docs: document APS5_STORE_AT_LABEL as a string that accepts only 1 StoreAtLabelRequested returns true for "1" and throws for any other value (expected 1), like its sibling APS5_STORE_AT_FLIP, so it is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 13dd311610..55aa309c4e 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -403,7 +403,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_STALE_EVICT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | also evicts a proved-present dead image's pending layers on an access | | `APS5_STALL_WATCHDOG_SECS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 0 | seconds a queue may go without progress before the submission rings are dumped; 0 disables the watchdog, at most 86400 | | `APS5_STORE_AT_FLIP` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stores every pending storage-image result at each display flip; only `1` is accepted | -| `APS5_STORE_AT_LABEL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | flushes every pending storage-image result when a label is written on the GPU | +| `APS5_STORE_AT_LABEL` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | flushes every pending storage-image result when a label is written on the GPU; only `1` is accepted | | `APS5_SUBMISSION_EPOCH_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | bumps the guest-memory collect epoch on every submission instead of only when the submission is newer than the last bump | | `APS5_SUBMIT_AT_END` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | submits recorded work at the end of a submission even when a worker is queued | | `APS5_SUBMIT_PROGRESS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp` | | enables opt-in pacing for eligible queue-zero submission prefixes within a shared nominal two-millisecond acknowledgement/timeline budget; timeout retains asynchronous acceptance | From b47d7e50d34f6445b88c681920f6605839439112 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 582/702] docs: document APS5_SRGB_SHADER_DECODE as a string TextureFormat.cpp compares the value with "1" and "copy" and ignores anything else, so setting it to an arbitrary value does not behave like a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 55aa309c4e..320328be69 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -398,7 +398,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | | `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | | `APS5_SRGB_ATTACHMENT_PROXY` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | forces the sRGB color-attachment proxy format; `1` forces it | -| `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | `1` forces shader sRGB decode for the sRGB 8-bit formats; `copy` does the same, except that 8 and 8_8 sRGB sample an RGBA8 sRGB copy when the device can blit to RGBA8 | +| `APS5_SRGB_SHADER_DECODE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | `1` forces shader sRGB decode for the sRGB 8-bit formats; `copy` does the same, except that 8 and 8_8 sRGB sample an RGBA8 sRGB copy when the device can blit to RGBA8; any other value has no effect | | `APS5_STAGING_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the device-local staging buffer pool; `0` disables the device tier | | `APS5_STALE_EVICT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | also evicts a proved-present dead image's pending layers on an access | | `APS5_STALL_WATCHDOG_SECS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 0 | seconds a queue may go without progress before the submission rings are dumped; 0 disables the watchdog, at most 86400 | From d035b7afe301b447dbc54e2c3f4a0c13bb749f6d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 583/702] docs: describe the perf pair switches as recompiler switches APS5_NO_PERF_FRONTEND_PAIR and APS5_NO_PERF_LAYOUT_PAIR are read by the shader recompiler itself, which the shader disk cache lists among its output-neutral switches, not only by the shader-memory test the rows pointed at. --- docs/dev/ENVIRONMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 320328be69..e1414b8a66 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -297,8 +297,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_PATTERN_FILL_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | disables the dword-pattern fill HLE | | `APS5_NO_PAUSE_SPIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | disables the CPU pause-spin in a blocked memory wait | | `APS5_NO_PENDING_HTILE_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | reads the HTILE words of a depth target bind even when recorded fills are pending over them, instead of working the words out from the fills | -| `APS5_NO_PERF_FRONTEND_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables frontend-pair reuse in the shader-memory test | -| `APS5_NO_PERF_LAYOUT_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables layout-pair reuse in the shader-memory test | +| `APS5_NO_PERF_FRONTEND_PAIR` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the recompiler's frontend-pair reuse; the shader disk cache treats it as output-neutral | +| `APS5_NO_PERF_LAYOUT_PAIR` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the recompiler's layout-pair reuse; the shader disk cache treats it as output-neutral | | `APS5_NO_PIPELINED_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | drains the draw pipeline at every CPU-path indirect draw instead of queuing it behind the pipeline | | `APS5_NO_PIPELINE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | | builds a pipeline per draw instead of reusing cached ones | | `APS5_NO_PIPELINE_JOURNAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | | keeps the pipeline journal off; 0 leaves it on, and it is also off while the shader disk cache is disabled | From fb7b465ad7e723b61a137c5f50fba95cf351d3f2 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 584/702] docs: point APS5_SPIRV_OPT at its production reader The SPIR-V backend reads the variable; the test only repeats the read. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index e1414b8a66..f1c5b74504 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -396,7 +396,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SKIP_SHADER_FILE` | path | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | file of whitespace-separated hex program addresses (`#` starts a comment) whose draws and dispatches are dropped; re-read within 500 ms of a change; a token that is not a whole 64-bit hex number is reported and ignored | | `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | | `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | -| `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | +| `APS5_SPIRV_OPT` | string | `core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | | `APS5_SRGB_ATTACHMENT_PROXY` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | forces the sRGB color-attachment proxy format; `1` forces it | | `APS5_SRGB_SHADER_DECODE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | `1` forces shader sRGB decode for the sRGB 8-bit formats; `copy` does the same, except that 8 and 8_8 sRGB sample an RGBA8 sRGB copy when the device can blit to RGBA8; any other value has no effect | | `APS5_STAGING_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the device-local staging buffer pool; `0` disables the device tier | From 29b1aaa480d936a2961f974575775c777795f833 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 585/702] docs: say what APS5_TRACE_SKIPS adds reportSkip already writes each distinct skip once; the variable only enables the 'again' lines for repeats. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index f1c5b74504..cb80a6268d 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -450,7 +450,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_SHADER_CORRELATION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | traces shader selection and preparation failure tokens through draw batches (requires `APS5_CAPTURE_TRACE`) | | `APS5_TRACE_SHADER_REGS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prints a shader's registers for one hex program address, or `all` | | `APS5_TRACE_SHADOW` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | prints the first n shadow publishes, the first 64 seeds, every evict and retire | -| `APS5_TRACE_SKIPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | | reports each distinct shader skip once | +| `APS5_TRACE_SKIPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | | reports each distinct shader skip once with or without it; set, a repeat of an already reported skip is also logged as `again`, cut to 100 characters | | `APS5_TRACE_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | prints every distinct staged range once, with its size and whether an atomic element lies in it | | `APS5_TRACE_SUMMARY_EVERY` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | seconds between periodic trace summaries, emitted on the next present | | `APS5_TRACE_TEXTURES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | prints a sampled texture's bytes and how many are nonzero | From 11a036fa09ad2a44203071fb8f1ef57f35a74063 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 586/702] docs: state the interval limit of APS5_TRACE_BENCH TraceBench::Keep retains MaximumIntervals = 240 and counts the rest as dropped. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index cb80a6268d..202db2952b 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -423,7 +423,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_THREAD_RT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 10 | realtime priority the raised threads ask rtkit for (clamped to 1..99) | | `APS5_THREAD_RTTIME_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 200000 | hard RLIMIT_RTTIME the realtime plan lowers to, in microseconds | | `APS5_TRACE_BARRIERS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | prints the first n accesses for which the hazard validator would skip a barrier; needs `APS5_BARRIER_VALIDATE` | -| `APS5_TRACE_BENCH` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | `1` with `APS5_TRACE_SUMMARY_EVERY` also rewrites `aps5-bench-<commit>-<pid>.json.gz` with every interval's frame times; any other value, or `1` without `APS5_TRACE_SUMMARY_EVERY`, fails the first present | +| `APS5_TRACE_BENCH` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | `1` with `APS5_TRACE_SUMMARY_EVERY` also rewrites `aps5-bench-<commit>-<pid>.json.gz` with the frame times of the last 240 intervals (older ones are counted as dropped); any other value, or `1` without `APS5_TRACE_SUMMARY_EVERY`, fails the first present | | `APS5_TRACE_BIGBUF` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | lists every copied guest region of at least 1 MiB | | `APS5_TRACE_CAPSYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | prints every unlocked hook wait | | `APS5_TRACE_CONTEXT_STATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | logs context-state operations and the context registers written | From 8ff8758c3760cae2e96369402201bd3864691c80 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 587/702] docs: document APS5_TIME_SCALE as a number string The value is parsed with strtod, so fractional factors work, as the row already says; the integer kind contradicted it. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 202db2952b..40a5f728bd 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -605,7 +605,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | disables pinning of job worker threads | | `APS5_NO_SLEEP_WAKE` | presence | `core/libs/prx/libkernel/Time/SleepWake.cpp` | | keeps every sleep of 1 ms or less in full instead of ending it at a GPU batch, batch completion action or flip completion | | `APS5_NO_TIMER_RESOLUTION` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | keeps the default 15.6 ms scheduler tick instead of raising it to 0.5 ms on Windows | -| `APS5_TIME_SCALE` | integer | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps and host JobManager worker spin intervals still take real time; fractional factors work, and a value that is not above 0 uses 1 | +| `APS5_TIME_SCALE` | string | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps and host JobManager worker spin intervals still take real time; fractional factors work, and a value that is not above 0 uses 1 | | `APS5_TRACE_APR` | presence | `core/libs/prx/libkernel/Apr/src/Apr.cpp` | | logs every APR file read with its path, offset, size and destination | | `APS5_TRACE_MEMORY` | presence | `core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp` | | logs direct-memory map, remap, protect, unmap and physical allocation operations | | `APS5_TRACE_SYNC` | presence | `core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp` | | logs event-flag and semaphore operations | From e6410e8daa461a134d391dd5315c2ae1cce8c870 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 588/702] docs: mark the Windows-only debug variables The timer selftest, the job worker pinning and the page query trace are compiled only under _WIN32; elsewhere the variables are ignored. --- docs/dev/ENVIRONMENT.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 40a5f728bd..710dca6556 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -444,7 +444,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_INDIRECT_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp` | | prints indirect draw packets that reach the precheck | | `APS5_TRACE_LATE_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | prints how late labels were trusted or refused | | `APS5_TRACE_PREPARATION_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prints where a demanded shader preparation sat in the queue | -| `APS5_TRACE_QUERY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | prints every 16th page query with its cost | +| `APS5_TRACE_QUERY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | on Windows, prints every 16th page query with its cost | | `APS5_TRACE_RESOURCE_KEY_MISSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | reports shader-resource cache key misses | | `APS5_TRACE_SECONDS` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | records from the first present `<start>` seconds after the first present for `<length>` seconds (whole numbers, `start:length`) | | `APS5_TRACE_SHADER_CORRELATION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | traces shader selection and preparation failure tokens through draw batches (requires `APS5_CAPTURE_TRACE`) | @@ -599,17 +599,17 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti |------|------|--------|---------|-------------| | `ANYPS5_LOADEXEC_ARGV_SKIP` | string | `core/libs/prx/libkernel/System/src/Process.cpp` | | on Windows, set to `1` to drop the first process argument and clear the variable | | `APS5_COARSE_TIMED_WAITS` | presence | `core/libs/prx/libkernel/Time/TimedWait.cpp` | | on Windows, rounds timed waits up to the scheduler tick | -| `APS5_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | pins the title's job worker threads to the efficiency cores of a hybrid CPU | -| `APS5_JOB_AFFINITY_MASK` | integer | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | hex core mask that job worker threads are pinned to | +| `APS5_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | on Windows, pins the title's job worker threads to the efficiency cores of a hybrid CPU | +| `APS5_JOB_AFFINITY_MASK` | integer | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | on Windows, hex core mask that job worker threads are pinned to | | `APS5_NO_DIRECT_MEMORY_SECTION_POOL` | presence | `core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp` | | on Windows, creates a section for every direct-memory allocation and closes it at release instead of reusing released sections that never held guest data | -| `APS5_NO_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | disables pinning of job worker threads | +| `APS5_NO_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | on Windows, disables pinning of job worker threads | | `APS5_NO_SLEEP_WAKE` | presence | `core/libs/prx/libkernel/Time/SleepWake.cpp` | | keeps every sleep of 1 ms or less in full instead of ending it at a GPU batch, batch completion action or flip completion | | `APS5_NO_TIMER_RESOLUTION` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | keeps the default 15.6 ms scheduler tick instead of raising it to 0.5 ms on Windows | | `APS5_TIME_SCALE` | string | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps and host JobManager worker spin intervals still take real time; fractional factors work, and a value that is not above 0 uses 1 | | `APS5_TRACE_APR` | presence | `core/libs/prx/libkernel/Apr/src/Apr.cpp` | | logs every APR file read with its path, offset, size and destination | | `APS5_TRACE_MEMORY` | presence | `core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp` | | logs direct-memory map, remap, protect, unmap and physical allocation operations | | `APS5_TRACE_SYNC` | presence | `core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp` | | logs event-flag and semaphore operations | -| `APS5_TRACE_TIMERS` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | runs a timed-wait selftest at startup and prints mean, p50 and max latencies | +| `APS5_TRACE_TIMERS` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | on Windows, runs a timed-wait selftest at startup and prints mean, p50 and max latencies | | `APS5_TRACE_USLEEP` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | reports every 2000 usleep calls the guest call sites that sleep most | | `APS5_TRACE_WAITS` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | aggregates blocking waits per guest call site and prints the longest-waiting sites every 3 s | From 1aac7817b5f2b1f997857a320e39d17321f24754 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 589/702] docs: keep the host read-only state in the USB passthrough row HostVolumes records ReadOnly from the mount options (or the Windows read-only volume flag) and the export reports it, so passthrough is not always writable. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 710dca6556..c72cb9ea91 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -550,7 +550,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `APS5_USB_STORAGE_PASSTHROUGH` | presence | `core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp` | | passes the FAT and exFAT volumes the host mounted from USB mass-storage devices through to the guest, writable; unset, no host volume is scanned and the guest sees no USB storage device | +| `APS5_USB_STORAGE_PASSTHROUGH` | presence | `core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp` | | passes the FAT and exFAT volumes the host mounted from USB mass-storage devices through to the guest, read-only where the host mounted them read-only; unset, no host volume is scanned and the guest sees no USB storage device | ### libSceVideoOut From c688fede63360b0381bc24667cfd5145858386da Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 590/702] docs: give the range and default of APS5_TRACE_EVENTS TraceThread::ParseCapacity rejects 0 and anything above 4,194,304, and DefaultCapacity is 262,144. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index c72cb9ea91..d3b0969f17 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -435,7 +435,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | prints one line per draw with its phases and inputs | | `APS5_TRACE_DRAW_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp` | | reports draw-cache entries whose key changed | | `APS5_TRACE_DRAW_PROGRAMS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | prints one line per recorded draw with the stage, role and code address of each program | -| `APS5_TRACE_EVENTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp` | | number of events a windowed trace keeps in its buffer | +| `APS5_TRACE_EVENTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp` | | number of events a windowed trace keeps in its buffer, 1 through 4,194,304; the default is 262,144 | | `APS5_TRACE_FILL_COVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | prints the first fills that meet images without covering them | | `APS5_TRACE_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | names what forces pending storage-image results into guest memory | | `APS5_TRACE_FRAMES` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | records `<count>` presents from present `<start>` (whole numbers, `start:count`) | From c95f8f80e063da08188e19d3847bd3cb4f1514b0 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:15:20 +0000 Subject: [PATCH 591/702] docs: say the APS5_SAVEDATA_TRACE limit is per logging site SAVEDATA_TRACE keeps a static counter in each use, so the default limit of three applies to every site separately. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d3b0969f17..cc8b0d6d30 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -538,7 +538,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `APS5_SAVEDATA_TRACE` | presence | `core/libs/prx/libSceSaveData.native/Export.cpp` | 3 | logs every save-data operation instead of the first three | +| `APS5_SAVEDATA_TRACE` | presence | `core/libs/prx/libSceSaveData.native/Export.cpp` | 3 | logs every save-data operation instead of the first three at each logging site | ### libSceSystemService From 91730a4f1ec34cb3a22eb35f590c5c3c3716c624 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:18:12 +0000 Subject: [PATCH 592/702] docs: say where --macos puts the bundled modules GuestModuleBuilder keeps each module's directory relative to the module parent under app0, as the Linux and Windows outputs do, so modules from sce_modules, prx or a --module-dir do not land in app0/sce_module. --- 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 54c1891397..e117d835bf 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -66,7 +66,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--help` | Print usage to stdout and exit with code `0`; input and output paths are not required. | | `--version` | Print the relinker build revision reported by `git describe` to stdout and exit with code `0`; input and output paths are not required. | | `--windows` | Produce a Windows PE executable. | -| `--macos` | Produce an x86-64 Mach-O executable, with bundled modules as Mach-O libraries in `app0/sce_module`. Conflicts with `--windows`. | +| `--macos` | Produce an x86-64 Mach-O executable, with bundled modules as Mach-O libraries under `app0/`, each in the directory it was found in (`sce_module`, `sce_modules`, `prx` or a `--module-dir`). Conflicts with `--windows`. | | `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | | `--windows-fixed-base <address>` | Map the lowest ELF load page at this fixed Windows address instead of the preferred `0x400000`, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | From 13019ed1f6086c938f8d94650b582585cdb44812 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:18:12 +0000 Subject: [PATCH 593/702] docs: state the thread limit of the Windows crash report ReportAllThreads captures at most 128 other threads and prints how many further threads were left out; the report did not list every thread of a larger process. --- 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 e117d835bf..cf9d5f8603 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -422,7 +422,7 @@ Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up Set `APS5_CRASH_STACK_BYTES=<n>` to print, after the stack return address candidates, the faulting thread's stack words from `rsp` for `n` bytes (at most 1048576, decimal or `0x` hexadecimal), four words per line with their offset from `rsp`. Below them the report prints eight words at every distinct stack word that points at readable, non-executable memory outside the thread's stack, labelled with the word's offset and the address it holds, up to 256 addresses. This shows the objects the caller frames kept, such as the `this` of a destructor or the buffer of a container. The dump stops at the first unreadable stack page, and nothing is printed when the variable is unset, empty or 0. -The report lists every other thread of the process with its `rip`, `rsp`, `rbp` and the return addresses of its `rbp` frame chain. A thread whose chain yields fewer than two frames, such as one waiting inside `ntdll.dll` or in code built without frame pointers, also gets up to 24 stack return address candidates: the words in the 32 KiB above its `rsp` that point at executable memory, read while the thread is suspended. Like the candidates of the faulting thread they are a scan, not an unwind, so stale return addresses of finished calls can appear among them. +The report lists the other threads of the process, up to 128 of them (a line says how many were left out), with their `rip`, `rsp`, `rbp` and the return addresses of its `rbp` frame chain. A thread whose chain yields fewer than two frames, such as one waiting inside `ntdll.dll` or in code built without frame pointers, also gets up to 24 stack return address candidates: the words in the 32 KiB above its `rsp` that point at executable memory, read while the thread is suspended. Like the candidates of the faulting thread they are a scan, not an unwind, so stale return addresses of finished calls can appear among them. ### GPU submission rings and stall dumps From 13da724480a0810e953d806b06848b74e6fde617 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:18:12 +0000 Subject: [PATCH 594/702] docs: say import_audit accepts converted guest.prx modules module_files yields a PE file whose name ends in .guest.prx and reads its exports; every other MZ file still raises an AuditError. --- 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 cf9d5f8603..528481ebcf 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -456,7 +456,7 @@ An import that names a library requires the literal export name. If its implemen Module registries mark ELF weak imports with `"weak": true`, allowing the audit's current-stop selection to skip them. Strong imports omit the field. Older module registries do not preserve weak bindings; regenerate them to distinguish optional imports. -ELF modules in `--modules` are audited regardless of filename: their undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries they need must have a file in `--libs` or `--modules`. Ordinary non-module sidecars are ignored; malformed ELF, PE/SELF containers and non-ELF files with module extensions remain errors. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. +ELF modules in `--modules` are audited regardless of filename: their undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries they need must have a file in `--libs` or `--modules`. Ordinary non-module sidecars are ignored; malformed ELF, SELF containers and non-ELF files with module extensions remain errors; the converted `*.guest.prx` PE modules of a Windows build are accepted, and any other PE file is an error. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. Each registry has a companion with the same prefix and the suffix `.needed.json`, listing its host library dependencies, including declared `DT_NEEDED` entries with no imports. Dependencies supplied by converted bundled modules, including their aliases, are omitted. The executable's companion includes host dependencies of its bundled modules. The audit reads these companions automatically; keep them beside the registries. Older registries without companions still work, but cannot report dependencies that have no imports; regenerate them with `--registry` to include those dependencies. From 2fe21dbd99c81e584be05920390b70b498ad5d75 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 595/702] docs: document APS5_SKIP_TIMING_TESTS as a string PreciseWait.cpp tests the first character for 0, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 13dd311610..a6c847352f 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -13,7 +13,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_POSIX_SPAWN_ROLE` | string | `core/libs/tests/GuestPosixSpawn.cpp` | | role of the test's own copy spawned as a `posix_spawn` child, which exits or sleeps on it and otherwise fails | | `ANYPS5_TEST_QUOTED_ARGV0` | presence | `core/libs/tests/GuestLoadExecHost.cpp` | | makes the load-exec test's first argument one with quotes, a backslash and non-ASCII text | | `APS5_EBOOT` | string | `core/libs/tests/GuestSystemExit.cpp` | | host path of the executable launched for /app0/eboot.bin, defaulting to the running executable | -| `APS5_SKIP_TIMING_TESTS` | presence | `core/libs/tests/PreciseWait.cpp` | | treats the timing tests' runner as preempted, so they skip without probing the clock; an empty value or one starting with 0 leaves the probe on | +| `APS5_SKIP_TIMING_TESTS` | string | `core/libs/tests/PreciseWait.cpp` | | treats the timing tests' runner as preempted, so they skip without probing the clock; an empty value or one starting with 0 leaves the probe on | | `APS5_USBD_DEVICE` | string | `core/libs/tests/GuestUsbd.cpp` | | selects the USB device a test drives, as vendor:product in hex | ### core/relinker From 89e8dbee00a263e562a6d9dead4209bebd7180ee Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 596/702] docs: document ANYPS5_NO_SHADER_CACHE as a string ShaderDiskCache.cpp ignores an empty value and 0, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index a6c847352f..688be9d97d 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -26,7 +26,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `ANYPS5_NO_SHADER_CACHE` | presence | `core/shader/recompiler/ShaderDiskCache.cpp` | | disables the on-disk shader cache when set to a non-empty value other than 0 | +| `ANYPS5_NO_SHADER_CACHE` | string | `core/shader/recompiler/ShaderDiskCache.cpp` | | disables the on-disk shader cache when set to a non-empty value other than 0 | | `ANYPS5_SHADER_CACHE_DIR` | string | `core/shader/recompiler/ShaderDiskCache.cpp` | | directory for the on-disk shader cache, overriding the one beside the executable | | `APS5_BDA_BYTE_READS` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp` | | forces one address lookup and byte load per byte for BDA reads instead of a wide dword load | | `APS5_BDA_DWORD_STORES` | presence | `core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp` | | reverts BDA writes to one address store per dword instead of a wide multi-dword store | From 017847c3bb89f216fc96f13579b798bc9dc3460e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 597/702] docs: document APS5_IMAGE_TABLE_STRICT as a string ResourceMaterializer.cpp compares the value with 0, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 688be9d97d..d2c69d2ab8 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -34,7 +34,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_EXACT_RCP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp` | | emits `v_rcp_f32` and `v_rcp_iflag_f32` as the RDNA hardware reciprocal (bit-exact for normal inputs and results) instead of the driver's division; adds about 13k SPIR-V words to each shader that uses them | | `APS5_GATHER_CONST` | integer | `core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp` | | integer every non-depth gather returns in all four texels instead of the sampled value | | `APS5_IMAGE_TABLE_KEY_SCAN` | integer | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | 256 | number of image-table entries the key scan examines | -| `APS5_IMAGE_TABLE_STRICT` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | sets image-table materialization to its strict form; 0 leaves it off | +| `APS5_IMAGE_TABLE_STRICT` | string | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | sets image-table materialization to its strict form; 0 leaves it off | | `APS5_NO_ATOMIC_BARRIER` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp` | | drops the device-scope acquire/release memory barrier emitted after atomics | | `APS5_NO_ATOMIC_ZERO_SKIP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp` | | restores plain buffer atomics instead of skipping identity-operand atomics | | `APS5_NO_CODE_HASH_KEY` | presence | `core/shader/recompiler/CacheKey.hpp` | | appends every code word to the cache key instead of hashing the code | From 71e4a80c7e1615683f379f6f3c115055c78dac42 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 598/702] docs: document APS5_STRICT_DESCRIPTOR_WEBS as a string ResourceTracker.cpp compares the value with 0, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d2c69d2ab8..0fcd741a95 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -56,7 +56,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SAVEEXEC_WRITE_FIRST` | presence | `core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp` | | writes the saved exec to the destination before reading the source in s_*_saveexec | | `APS5_SINGLE_LANE` | string | `core/shader/recompiler/Recompiler.cpp` | | hex code addresses (comma separated, or all) of wave64 programs kept at one guest lane per invocation | | `APS5_SRT_DEBUG` | presence | `core/shader/recompiler/Optimization/src/RequestMemoryView.cpp` | | prints extra SRT descriptor-walk debug output, such as guest reads outside the request memory | -| `APS5_STRICT_DESCRIPTOR_WEBS` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | makes a T# or S# read in a descriptor web throw when a path's descriptor dwords depend on the program counter, EXEC or a lane mask, instead of selecting the first descriptor and recording an `InvalidDescriptor` fault when a lane takes that path; 0 leaves it off | +| `APS5_STRICT_DESCRIPTOR_WEBS` | string | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | makes a T# or S# read in a descriptor web throw when a path's descriptor dwords depend on the program counter, EXEC or a lane mask, instead of selecting the first descriptor and recording an `InvalidDescriptor` fault when a lane takes that path; 0 leaves it off | | `APS5_TRACE_BDA` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | prints the first few accesses that make a program take the address-based path | | `APS5_TRACE_BUFFER_WRITTEN` | presence | `core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp` | | prints how many guest buffer elements a program may store to and how many it only loads | | `APS5_TRACE_IMAGE_TABLE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | traces image-table resolution | From c4239be9702fb9e331380b8243e9b2ad2fcf045a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 599/702] docs: document APS5_COPY_READ_TRACKING as a string Recorder.cpp compares the value with 0, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 0fcd741a95..f40f4228fa 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -110,7 +110,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_COPY_DESTINATION_STRICT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | refuses a CPU copy whose destination has any pending writer, not only an unsettled one | | `APS5_COPY_HLE_CPU_MAX` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | 262144 | largest copy in bytes the CPU side of the copy HLE handles | | `APS5_COPY_HLE_GPU_MAX` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | largest copy in bytes the GPU side of the copy HLE handles; unset means no limit | -| `APS5_COPY_READ_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | notes in-place reads of a range; set to 0 to disable, making the CPU copy path require an idle recorder | +| `APS5_COPY_READ_TRACKING` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | notes in-place reads of a range; set to 0 to disable, making the CPU copy path require an idle recorder | | `APS5_COPY_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | treats any pending writer over a copy's range as needing a wait | | `APS5_COPY_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | verifies a CPU copy's source and destination bytes against the pre-copy values after a sync | | `APS5_COPY_WAIT_SOURCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for the source's producer batch when it is the only one in flight, on non-zero queues | From 8cf29166bd86d4dbc1b65ddc402abcbf923959df Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 600/702] docs: document APS5_EXACT_DRAW_WRITES as a string WrittenBuffers.cpp compares the value with 0, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index f40f4228fa..40a40283e1 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -153,7 +153,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DUMP_TARGETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | 0 | dumps the first n renders of each non-resident color target as target_<address>_<n>.raw in the working directory; a non-zero value also runs every draw synchronously, and in practice it needs APS5_NO_RESIDENT_TARGETS=1 | | `APS5_DUMP_TEXTURE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | hex addresses or WxH extents whose sampled textures are dumped on each of their first 8 uploads as texture_<address>_<n>.raw | | `APS5_EAGER_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes back pending storage layers eagerly | -| `APS5_EXACT_DRAW_WRITES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp` | | sizes a color or depth target's draw write ranges from its own bytes and slices (the default when unset); `0` uses the slack and unknown-byte approximation instead | +| `APS5_EXACT_DRAW_WRITES` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp` | | sizes a color or depth target's draw write ranges from its own bytes and slices (the default when unset); `0` uses the slack and unknown-byte approximation instead | | `APS5_FILL_CHAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | seeds and doubles the fill pattern in place in the import | | `APS5_FILL_CLEAR_EXACT_ONLY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | turns a fill into a clear only when it covers the target exactly and nothing else | | `APS5_FILL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for every recorded store over the fill's range | From 13a973de6fc2f00caa181375378e537b1bde0bf8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 601/702] docs: document APS5_HOOK_FLUSH_CPU_BLOCKS as a string Recorder.cpp acts only on a value that starts with 0, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 40a40283e1..7e6a573b7a 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -173,7 +173,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_GROUP_CAPTURE_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp` | | resolves group-ahead capture on queue 0 as well | | `APS5_HANDOFF_SPIN_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp` | | microseconds a thread spins for a handed-off draw's work or for the GPU mutex before blocking, clamped to 0..10000; unset spins 150 us when the process has more than 8 usable processors and not at all otherwise | | `APS5_HEAP_MIRROR_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 24576 | budget in MiB for the heap mirrors held for registered allocations | -| `APS5_HOOK_FLUSH_CPU_BLOCKS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | set to 0 flushes pending images whose every 64 KiB block the CPU wrote, instead of skipping them | +| `APS5_HOOK_FLUSH_CPU_BLOCKS` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | set to 0 flushes pending images whose every 64 KiB block the CPU wrote, instead of skipping them | | `APS5_HOOK_FULL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes the flush hook's recorded-store wait a full Sync under the GpuMutex | | `APS5_HOOK_LOCKED_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | keeps the flush hook's pending-write wait under the GpuMutex | | `APS5_HOST_IMPORT_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 0 | budget in MiB for pinned host imports; `0` selects the larger of 6144 MiB and half the physical memory, at most the largest host heap minus the larger of 2 GiB and half that heap (4055 MiB on a 16 GB Windows PC with an RTX 4060) | From 4da9369e1789c8867aacaa3d7fea4e399976dfb3 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 602/702] docs: document APS5_KEYS_FILL_CLEAR as a string Texture.cpp acts only on the value 1, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7e6a573b7a..c229aced36 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -183,7 +183,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_INSERT_COMPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | compares a fresh capture's regions against the recorder before inserting it into the cache | | `APS5_KEPT_DESCRIPTOR_SETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 8 | number of freed dedicated image table descriptor sets kept for a later build with the same layout and sizes; 0 keeps none | | `APS5_KEYFLIP_DROP` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | drops a pending unit's results on a clear-code to uncompressed key flip, as before | -| `APS5_KEYS_FILL_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | clears an image from DCC keys whose fill pattern it matches (only when set to 1) | +| `APS5_KEYS_FILL_CLEAR` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | clears an image from DCC keys whose fill pattern it matches (only when set to 1) | | `APS5_LABEL_FLUSH_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | age in microseconds at which a pending label batch is recorded or submitted; unset is 1000 with batched label submission and 250 when `APS5_NO_LABEL_BATCH_SUBMIT` or `APS5_NO_LABEL_BATCHING` turns it off | | `APS5_LABEL_GATE_ALL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | gates a label store on any pending completion, not only one overlapping the range | | `APS5_LABEL_LOCK_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records each label under the GpuMutex instead of deferring it | From bd8a7b9315b64779ddf5acf2a02be35eb4126207 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 603/702] docs: document APS5_LABEL_TRUST_LATE as a string Recorder.cpp compares the value with 0, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index c229aced36..971a338ae4 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -192,7 +192,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_LABEL_RUNS_INLINE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records store runs at once and closes each run at the next command | | `APS5_LABEL_STORE_ALWAYS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | writes back every label store even when a later store already covered its bytes | | `APS5_LABEL_SUBMIT_NOW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | submits every label immediately instead of waiting for the flush deadline | -| `APS5_LABEL_TRUST_LATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | trusts cache entries with a stamp at or below the wait's stamp under the late rule (off unless set to a non-zero value) | +| `APS5_LABEL_TRUST_LATE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | trusts cache entries with a stamp at or below the wait's stamp under the late rule (off unless set to a non-zero value) | | `APS5_LABEL_TRY_EACH_PACKET` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | attempts to record deferred labels at every self-locking packet instead of deferring to its lock | | `APS5_LAND_WAITS_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | notes pending waits on queue 0 as well | | `APS5_LAZY_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares a registered shader on its first use instead of while registering it | From f281b020250415b386bc90dbafe7e0f6653fefef Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 604/702] docs: document APS5_LOG_STATS as a string AsyncLog.hpp compares the value with 0, so the value is interpreted and the variable is not a presence switch. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 971a338ae4..333c35f356 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -199,7 +199,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_LDS_SLACK` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp` | 0 | dwords added to every dispatch's LDS allocation | | `APS5_LOCKED_BUILD` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds the whole dispatch preparation under the GpuMutex, skipping the split path | | `APS5_LOCKED_DRAW_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | takes the GpuMutex before preparing a draw | -| `APS5_LOG_STATS` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp` | | prints the AGC log's line, byte and coalescing statistics every 10 s window by default, when unset or set to anything but `0`; `0` turns them off | +| `APS5_LOG_STATS` | string | `core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp` | | prints the AGC log's line, byte and coalescing statistics every 10 s window by default, when unset or set to anything but `0`; `0` turns them off | | `APS5_LOG_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp` | | writes AGC log lines on the calling thread instead of through the asynchronous sink thread | | `APS5_LOOP_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | enables the fault buffer that guards shader loops running past their bound | | `APS5_MIMALLOC` | presence | `core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp` | | loads `mimalloc.dll` in the Windows entry stub, so the process allocates through mimalloc instead of the C runtime's heap | From 85928676585b021fecdfde92c9cde34fab47a80e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 605/702] docs: correct the default of APS5_FRAME_CAPTURE_AT_FLIP FrameRecorder reads the variable with a fallback of 0 and raises it to 1 when it picks the boundary, so the effect of the default is the same as 1 but the default is 0; a value that is not a number throws. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 333c35f356..cc8aea5b4c 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -162,7 +162,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_FORCE_SDR` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp` | unset | suppresses advertised HDR availability while retaining detected host capabilities; uses the existing SDR presentation path | | `APS5_FORCE_STRUCTURIZER_FALLBACK` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | makes the structurizer use the reach-flag fallback for every graph that supports it instead of trying cloning first; the control-flow tests then expect that fallback | | `APS5_FRAME_CAPTURE` | string | `core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp` | | directory in which the driver records a few frames of the title's driver calls and guest memory for `agc_frame_replay` | -| `APS5_FRAME_CAPTURE_AT_FLIP` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp` | 1 | frame boundary (CPU flip or submission that flips) at which the frame capture starts when no trigger file is set | +| `APS5_FRAME_CAPTURE_AT_FLIP` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp` | 0 | frame boundary (CPU flip or submission that flips) at which the frame capture starts when no trigger file is set; 0 and 1 both start at the first boundary; a value that is not a number fails | | `APS5_FRAME_CAPTURE_FRAMES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp` | 10 | number of frame boundaries the frame capture records | | `APS5_FRAME_CAPTURE_TRIGGER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp` | | file whose appearance starts the frame capture at the next frame boundary; a number in it replaces the frame count | | `APS5_FULL_BARRIERS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every barrier instead of merging redundant ones | From 009be80e18837e89174c7317df0f30e0be29d03f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 606/702] docs: point APS5_EBOOT at the code that reads it libSceSystemService reads the variable when it prepares a launch, and an empty value counts as unset; GuestSystemExit.cpp only checks that it is not defined. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index cc8aea5b4c..7e38d789e8 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -12,7 +12,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_LOADEXEC_TEST_ROOT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | directory the load-exec host test reads its guest executable from | | `ANYPS5_POSIX_SPAWN_ROLE` | string | `core/libs/tests/GuestPosixSpawn.cpp` | | role of the test's own copy spawned as a `posix_spawn` child, which exits or sleeps on it and otherwise fails | | `ANYPS5_TEST_QUOTED_ARGV0` | presence | `core/libs/tests/GuestLoadExecHost.cpp` | | makes the load-exec test's first argument one with quotes, a backslash and non-ASCII text | -| `APS5_EBOOT` | string | `core/libs/tests/GuestSystemExit.cpp` | | host path of the executable launched for /app0/eboot.bin, defaulting to the running executable | +| `APS5_EBOOT` | string | `core/libs/prx/libSceSystemService/Export.cpp` | | host path of the executable launched for /app0/eboot.bin; unset or empty means the running executable | | `APS5_SKIP_TIMING_TESTS` | string | `core/libs/tests/PreciseWait.cpp` | | treats the timing tests' runner as preempted, so they skip without probing the clock; an empty value or one starting with 0 leaves the probe on | | `APS5_USBD_DEVICE` | string | `core/libs/tests/GuestUsbd.cpp` | | selects the USB device a test drives, as vendor:product in hex | From dfe6db8b5818e36f7cb37c038de57a5fa815b817 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:22:48 +0000 Subject: [PATCH 607/702] docs: spell out the APS5_DISPATCH_CACHE_ENTRIES default dispatchCacheEntries picks 8192 only for more than two variants, 16384 for one or two and 4096 when stamp validation is on. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7e38d789e8..78c508bbf8 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -124,7 +124,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DEPTH_PLANE_COPY_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | 256 | budget in MiB of the sampled depth-plane copies kept for reuse (least recently used copies leave first); `0` keeps every copy | | `APS5_DEPTH_PLANE_WHOLE_STORE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | stores a stale depth plane whole when memory over it is read, instead of only the 64 KiB units the read overlaps | | `APS5_DISPATCH_CACHE_BYTES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 268435456 | size of the dispatch cache in bytes (0 keeps the default of 256 MiB) | -| `APS5_DISPATCH_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 8192 | number of dispatch cache entries; 0 picks a default from the variant count | +| `APS5_DISPATCH_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 8192 | number of dispatch cache entries; 0 picks a default from the variant count: 8192 with the default 4 variants, 16384 with 2 or fewer, 4096 under `APS5_DISPATCH_STAMP_VALIDATE` | | `APS5_DISPATCH_STAMP_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | validates dispatch cache hits by write stamps, forcing one variant and a 4096-entry cache | | `APS5_DISPATCH_VARIANTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 4 | number of dispatch variants kept per cache entry, clamped to the maximum | | `APS5_DISPMOVE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1024 | upper bound in KiB on the read-only buffers a dispatch-cache hit moves into its template, above which the dispatch is prepared again | From 953c3a798f74f532f70de30f675f4560c6b79699 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:23:25 +0000 Subject: [PATCH 608/702] docs: give the real size of the prebuild thread pool RegistrationPreparePool::Prebuild uses APS5_PREBUILD_THREADS (at least 1) when set and otherwise max(2, hardware_concurrency() / 2) threads, not one. --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index e63cb8cf20..d86b91309f 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -134,7 +134,7 @@ flowchart LR - 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. - `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. A compute or pixel stage's explicit ABI resolution (`ResolveShaderAbi`) plans the stage with the ABI's state, records the ABI and leaves its preparation to the shader's first use, which prepares it with that state (`PreparedShaders::deferredAbis`), so a shader that never draws or dispatches stays unprepared; a graphics-stage ABI is still prepared when it is resolved. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. -- 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. Ahead-of-time dispatch resolution (`resolveGroupAhead` in [`DispatchAhead.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp)) scans the upcoming dispatch run up to the next wait or synchronization packet, moves still-pending registered compute shaders to the front of the registration preparation queue (`PrioritizeRegistrationPreparation`) and queues preparation (`SubmitRegistrationPreparation`) for registered compute shaders that have no prepared entries yet (such as under `APS5_LAZY_SHADER_PREPARE=1`) with the previewed register state and entry offset; when a dispatch resolved ahead misses the dispatch cache in [`Dispatch.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp), `PrebuildComputePipeline` is submitted to the same `APS5_PREBUILD_THREADS` pool so the compute `VkPipeline` is built before the worker reaches the packet. +- 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 pool of `max(2, hardware_concurrency() / 2)` threads (`APS5_PREBUILD_THREADS` sets the count, at least 1) 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. Ahead-of-time dispatch resolution (`resolveGroupAhead` in [`DispatchAhead.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp)) scans the upcoming dispatch run up to the next wait or synchronization packet, moves still-pending registered compute shaders to the front of the registration preparation queue (`PrioritizeRegistrationPreparation`) and queues preparation (`SubmitRegistrationPreparation`) for registered compute shaders that have no prepared entries yet (such as under `APS5_LAZY_SHADER_PREPARE=1`) with the previewed register state and entry offset; when a dispatch resolved ahead misses the dispatch cache in [`Dispatch.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp), `PrebuildComputePipeline` is submitted to the same `APS5_PREBUILD_THREADS` pool so the compute `VkPipeline` is built before the worker reaches the packet. - 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 stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. - 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. From 7e5831fcced8e8b770b354973e8a6433b9ee9a8d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:23:26 +0000 Subject: [PATCH 609/702] docs: say APS5_DRAW_PIPELINE is tested for being set ShaderMemory.cpp and Draw.cpp enable the draw pipeline whenever the variable exists, unlike APS5_PIPELINE_DISPATCH, which treats an empty value and 0 as off. --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index d86b91309f..7ea15c7161 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -130,7 +130,7 @@ flowchart LR - A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=<n>` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. - Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An indirect draw resolved on the CPU (its records read by the worker) drains only when its records or its count overlap a queued write; the records it draws inline drain first, and its last drawn record is queued like a direct draw (`APS5_NO_PIPELINED_INDIRECT=1` drains at every indirect draw). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. -- `APS5_DRAW_PIPELINE=1` (off by default, meant with `APS5_PIPELINED_DRAWS=32 APS5_PIPELINE_DISPATCH=1`) takes more of a draw off the worker. A `DrawFinisher` thread runs the second half of a direct pipelined draw (stage materialization, the draw cache insert and the hand-off to the committer) while the worker reads the next packets; the worker drains it before the next draw's cache lookup, before packets other than register writes and draws, and before guest reads, and finishes in place a draw whose stages may write memory (`ProgramWritesMemory`) or write through the BDA page table. The committer skips the fragment-output validation and storage-image layer scans whose inputs match its last commit (`CommitTrims`), and a resource or storage image proved current keeps its proof for the rest of the collect epoch until `GuestMemory::BreakProofs` (a CPU write, a write-back, a recorded write) moves `ProofSerial`. Color metadata passes go through the pipeline with their write ranges (`PipelinedColorMetadataPassWrites`) instead of draining it. A data-only hit whose shape entry belongs to an older draw replaces that entry in place at the second sight of the same read user words. On Linux the main image's pages get a page table entry, and capture pages are pooled across threads. `APS5_VERIFY_DRAW_PIPELINE=1` runs a full check next to 1 in 64 reused proofs and prints `[epoch-proofs]` counts. +- `APS5_DRAW_PIPELINE` (any value turns it on, even `0`; off by default, meant with `APS5_PIPELINED_DRAWS=32 APS5_PIPELINE_DISPATCH=1`) takes more of a draw off the worker. A `DrawFinisher` thread runs the second half of a direct pipelined draw (stage materialization, the draw cache insert and the hand-off to the committer) while the worker reads the next packets; the worker drains it before the next draw's cache lookup, before packets other than register writes and draws, and before guest reads, and finishes in place a draw whose stages may write memory (`ProgramWritesMemory`) or write through the BDA page table. The committer skips the fragment-output validation and storage-image layer scans whose inputs match its last commit (`CommitTrims`), and a resource or storage image proved current keeps its proof for the rest of the collect epoch until `GuestMemory::BreakProofs` (a CPU write, a write-back, a recorded write) moves `ProofSerial`. Color metadata passes go through the pipeline with their write ranges (`PipelinedColorMetadataPassWrites`) instead of draining it. A data-only hit whose shape entry belongs to an older draw replaces that entry in place at the second sight of the same read user words. On Linux the main image's pages get a page table entry, and capture pages are pooled across threads. `APS5_VERIFY_DRAW_PIPELINE=1` runs a full check next to 1 in 64 reused proofs and prints `[epoch-proofs]` counts. - 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. - `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. A compute or pixel stage's explicit ABI resolution (`ResolveShaderAbi`) plans the stage with the ABI's state, records the ABI and leaves its preparation to the shader's first use, which prepares it with that state (`PreparedShaders::deferredAbis`), so a shader that never draws or dispatches stays unprepared; a graphics-stage ABI is still prepared when it is resolved. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. From b04ac8eca0507d7e81ae84e6a8a0ebe12facc234 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:26:06 +0000 Subject: [PATCH 610/702] docs: list every runtime DLL the hardware oracle loads oracle_hip.py tries amdhip64_7, amdhip64_6 and amdhip64, and amd_comgr_3, amd_comgr_2 and amd_comgr, newest first; the guide named only the older ones. --- docs/dev/HW_ORACLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/HW_ORACLE.md b/docs/dev/HW_ORACLE.md index c675934529..8995a5f9de 100644 --- a/docs/dev/HW_ORACLE.md +++ b/docs/dev/HW_ORACLE.md @@ -24,7 +24,7 @@ from the GPU (`HW_ORACLE_TARGET` overrides it). ### Windows -- AMD's Windows driver, which installs the HIP runtime (`amdhip64_6.dll`) and the code object manager (`amd_comgr_2.dll`) +- AMD's Windows driver, which installs the HIP runtime (`amdhip64_7.dll`, `amdhip64_6.dll` or `amdhip64.dll`) and the code object manager (`amd_comgr_3.dll`, `amd_comgr_2.dll` or `amd_comgr.dll`) in `System32`. ROCm, a C compiler, `clang` and `ld.lld` are not needed. - Python 3 from python.org or the Microsoft Store. From 1eb3a8cf3f608cc6da3b4d56ff1ce9eb91ec313a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:26:06 +0000 Subject: [PATCH 611/702] docs: list the APS5_TRACE_UDS switch libSceNpUniversalDataSystem and libSceNpTrophy2 read it and print [uds] lines to stderr, but the tracing guide did not list it. --- docs/dev/TRACING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index 57168055f8..b9a5f8aa59 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -20,6 +20,7 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_SSE4A` | Each emulated SSE4A instruction once per guest instruction address (Windows). | | `APS5_TRACE_SYNC` | Event-flag operations and their wait and wake results. | | `APS5_TRACE_TIMERS` | Timing measurements for the host wait primitives used by the emulator. | +| `APS5_TRACE_UDS` | Universal Data System events posted to a context, and the trophy title data it cannot find. | | `APS5_TRACE_USLEEP` | Every 2,000 guest `usleep` calls, with caller addresses and requested delays. | | `APS5_TRACE_VIDEODEC` | Video decoder API calls and decode results. | | `APS5_TRACE_WAITS` | Blocking waits aggregated by guest call site, including timeout counts and longest waits, every three seconds. | From eca9bedd3707aee47c344d76ac528c19ec2766db Mon Sep 17 00:00:00 2001 From: Mohamed Malek Aifa <32399935+malekaifa@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:43:11 +0100 Subject: [PATCH 612/702] fix(tools): protect symbolizer module inputs --- tools/symbolize_samples.py | 25 +++++ tools/tests/test_symbolize_samples.py | 144 ++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) diff --git a/tools/symbolize_samples.py b/tools/symbolize_samples.py index 7a6f9ffead..c78ff4e18e 100644 --- a/tools/symbolize_samples.py +++ b/tools/symbolize_samples.py @@ -291,6 +291,31 @@ def main(argv=None): samples = load(args.samples, args.thread) if args.since: samples = window(samples, load(args.since, args.thread)) + if args.out and samples.stacks: + referenced_modules = {name.lower() for name, _ in samples.self_counts} + referenced_modules.update(name.lower() for name, _ in samples.inclusive) + for frames in samples.stacks: + for frame in frames.split(";"): + name, separator, _ = frame.partition("+0x") + if separator: + referenced_modules.add(name.lower()) + module_output_alias = False + for directory in args.libs: + for source in Path(directory).iterdir(): + if not source.is_file() or not (source.resolve() == output.resolve() or + (source.exists() and output.exists() and source.samefile(output))): + continue + if source.name.lower() in referenced_modules or source.suffix.lower() in (".prx", ".dll", ".exe"): + module_output_alias = True + continue + try: + with source.open("rb") as module: + if module.read(2) == b"MZ": + module_output_alias = True + except OSError: + module_output_alias = True + if module_output_alias: + parser.error("symbolized output must be separate from module inputs") symbolizer = Symbolizer(args.libs, samples.headers, args.demangle, warn=lambda text: print("warning: " + text, file=sys.stderr), unverified=args.unverified) for line in summarize(symbolizer, samples, args.top): print(line) diff --git a/tools/tests/test_symbolize_samples.py b/tools/tests/test_symbolize_samples.py index 17825f69a4..b9fba34664 100644 --- a/tools/tests/test_symbolize_samples.py +++ b/tools/tests/test_symbolize_samples.py @@ -109,6 +109,150 @@ class SymbolizeSamplesTests(unittest.TestCase): for path, content in original.items(): self.assertEqual(path.read_bytes(), content) + def test_output_cannot_alias_a_module_input(self): + samples = self.root / "worker.collapsed.txt" + samples.write_text( + f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n" + "libSceAgcDriver.prx+0x1011 1\n", + encoding="utf-8", + ) + module = self.libs / "libSceAgcDriver.prx" + original = module.read_bytes() + for alias in ("same", "hardlink"): + with self.subTest(alias=alias): + output = module if alias == "same" else self.root / "module-output" + if alias == "hardlink": + os.link(module, output) + result = subprocess.run([sys.executable, symbolize_samples.__file__, str(samples), + "--libs", str(self.libs), "--out", str(output)], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("must be separate from module inputs", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(module.read_bytes(), original) + if alias == "hardlink": + output.unlink() + + def test_output_cannot_alias_an_unreferenced_module(self): + samples = self.root / "worker.collapsed.txt" + samples.write_text( + f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n" + "libSceAgcDriver.prx+0x1011 1\n", + encoding="utf-8", + ) + module = self.libs / "other.prx" + build_pe(module) + original = module.read_bytes() + for alias in ("same", "hardlink"): + with self.subTest(alias=alias): + output = module if alias == "same" else self.root / "other-output" + if alias == "hardlink": + os.link(module, output) + result = subprocess.run([sys.executable, symbolize_samples.__file__, str(samples), + "--libs", str(self.libs), "--out", str(output)], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("must be separate from module inputs", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(module.read_bytes(), original) + if alias == "hardlink": + output.unlink() + + def test_write_only_aliased_module_candidate_is_protected(self): + if os.name != "posix" or os.geteuid() == 0: + self.skipTest("An unprivileged POSIX user is required") + samples = self.root / "worker.collapsed.txt" + samples.write_text( + f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n" + "libSceAgcDriver.prx+0x1011 1\n", + encoding="utf-8", + ) + output = self.libs / "opaque-module" + output.write_bytes(b"opaque") + original = output.read_bytes() + output.chmod(0o200) + try: + result = subprocess.run([sys.executable, symbolize_samples.__file__, str(samples), + "--libs", str(self.libs), "--out", str(output)], + capture_output=True, text=True, timeout=5) + finally: + output.chmod(0o600) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("module inputs", result.stderr) + self.assertEqual(output.read_bytes(), original) + + def test_unreferenced_malformed_or_unsupported_module_candidate_is_protected(self): + samples = self.root / "worker.collapsed.txt" + samples.write_text( + f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n" + "libSceAgcDriver.prx+0x1011 1\n", + encoding="utf-8", + ) + for kind in ("malformed-prx", "unsupported-pe"): + with self.subTest(kind=kind): + output = self.libs / ("malformed.prx" if kind == "malformed-prx" else "unsupported.bin") + if kind == "malformed-prx": + output.write_bytes(b"not a PE image") + else: + build_pe(output) + image = bytearray(output.read_bytes()) + struct.pack_into("<H", image, 0x58, 0x10B) + output.write_bytes(image) + original = output.read_bytes() + result = subprocess.run([sys.executable, symbolize_samples.__file__, str(samples), + "--libs", str(self.libs), "--out", str(output)], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("module inputs", result.stderr) + self.assertEqual(output.read_bytes(), original) + + def test_address_output_alias_keeps_collapsed_stack_error(self): + samples = self.root / "worker.txt" + samples.write_text( + f"# 1 samples\nlibSceAgcDriver.prx 0x1011 1 1\n" + f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n", + encoding="utf-8", + ) + module = self.libs / "libSceAgcDriver.prx" + original = module.read_bytes() + result = subprocess.run([sys.executable, symbolize_samples.__file__, str(samples), + "--libs", str(self.libs), "--out", str(module)], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("--out needs a collapsed-stack file", result.stderr) + self.assertNotIn("module inputs", result.stderr) + self.assertEqual(module.read_bytes(), original) + + def test_unreferenced_output_inside_libs_remains_allowed(self): + samples = self.root / "worker.collapsed.txt" + samples.write_text( + f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n" + "libSceAgcDriver.prx+0x1011 1\n", + encoding="utf-8", + ) + output = self.libs / "symbolized.txt" + output.write_text("previous output\n", encoding="utf-8") + result = subprocess.run([sys.executable, symbolize_samples.__file__, str(samples), + "--libs", str(self.libs), "--out", str(output)], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(output.read_text(encoding="utf-8"), "libSceAgcDriver.prx!short 1\n") + + def test_module_alias_does_not_hide_a_later_missing_lib_directory(self): + samples = self.root / "worker.collapsed.txt" + samples.write_text( + f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n" + "libSceAgcDriver.prx+0x1011 1\n", + encoding="utf-8", + ) + module = self.libs / "libSceAgcDriver.prx" + missing = self.root / "missing-libs" + original = module.read_bytes() + with self.assertRaises(FileNotFoundError): + symbolize_samples.main([str(samples), "--libs", str(self.libs), + "--libs", str(missing), "--out", str(module)]) + self.assertEqual(module.read_bytes(), original) + def test_distinct_output_preserves_sample_captures(self): record = f"# module libSceAgcDriver.prx {SIZE_OF_IMAGE:#x} {TIMESTAMP:#x}\n" later, earlier, output = (self.root / name for name in ("later.txt", "earlier.txt", "named.txt")) From 3ef5fc044ba6ffe2a29b399aa3d84605f3e0ee39 Mon Sep 17 00:00:00 2001 From: Mohamed Malek Aifa <32399935+malekaifa@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:56:36 +0100 Subject: [PATCH 613/702] fix(tools): report deeply nested release notes JSON --- tools/prepare_release_notes.py | 5 ++++- tools/tests/test_package_release.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index 0077730684..015c2911fe 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -27,7 +27,10 @@ def prepare_notes(source, body, full): for second in paths[index + 1:]: if first.resolve() == second.resolve() or (first.exists() and second.exists() and first.samefile(second)): raise ValueError("Release note input and outputs must be distinct files") - payload = json.loads(source.read_text(encoding="utf-8-sig")) + try: + payload = json.loads(source.read_text(encoding="utf-8-sig")) + except RecursionError as error: + raise ValueError("Generated release notes response is too deeply nested") from error if not isinstance(payload, dict) or "body" not in payload: raise ValueError("Generated release notes response must be a JSON object with a body field") notes = payload["body"] diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 9c061dfaf9..4216bc439c 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -795,6 +795,25 @@ class ReleaseNotesEdgeCaseTests(unittest.TestCase): self.assertEqual(body_path.read_text(encoding="utf-8"), "pre-existing body") self.assertEqual(full_path.read_text(encoding="utf-8"), "pre-existing full") + def test_main_reports_error_for_json_recursion(self): + with tempfile.TemporaryDirectory() as directory: + directory = Path(directory) + source_path = directory / "source.json" + source_path.write_text(json.dumps({"body": "notes"}), encoding="utf-8") + body_path = directory / "body.md" + body_path.write_text("pre-existing body", encoding="utf-8") + full_path = directory / "full.md" + full_path.write_text("pre-existing full", encoding="utf-8") + stderr = io.StringIO() + with patch.object(prepare_release_notes.json, "loads", side_effect=RecursionError("decoder recursion")): + with contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit) as error: + prepare_release_notes.main(["--input", str(source_path), "--body", str(body_path), "--full", str(full_path)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("response is too deeply nested", stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + self.assertEqual(body_path.read_text(encoding="utf-8"), "pre-existing body") + self.assertEqual(full_path.read_text(encoding="utf-8"), "pre-existing full") + def test_main_reconfigures_streams(self): class Stream: def __init__(self): From db6d1c5dd2415551a7cb45d75017d89afffcc3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <adriadam10@gmail.com> Date: Tue, 27 Oct 2026 22:42:08 +0100 Subject: [PATCH 614/702] fix(main): keep the stopped spawn child alive until its continued status is read Run 40579197008 (main 771d8b5bf7) failed guest_posix_spawn. Under load the child of CheckStoppedChild exits right after SIGCONT, so waitpid(WCONTINUED) reports the exit instead of 0x13 (reproduced with 24 CPU spinners via ctest --repeat). The child now sleeps 2 s after the stop. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestPosixSpawn.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index fad3a09587..473fb160c1 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -346,6 +346,7 @@ int main() { #ifndef _WIN32 if (std::strcmp(role, "child-stop") == 0) { std::raise(SIGSTOP); + std::this_thread::sleep_for(std::chrono::seconds(2)); std::_Exit(44); } if (std::strcmp(role, "child-kill-bus") == 0) std::raise(SIGBUS); From f8ddd30ce16ad4e7950a710b7fe5c055a344e941 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Tue, 27 Oct 2026 16:40:45 -0500 Subject: [PATCH 615/702] test(check-compile): keep slow syntax check cases within the CI time limit test_library_directory_values_can_look_like_modes passed -L -E as mode flags, so check_modes took the -E for a preprocessing mode and dropped -c. Its control commands linked an executable with each compiler, which on the Windows runner goes through link.exe; the case took about twice as long as its -I -E counterpart and timed out in 6 Windows CI jobs. The -L -E pair now goes through configure like the include directory case, so the control only compiles. The command check_compile checks is the same, since it drops -c. test_response_options_compile_without_changing_build_outputs ran 30 check_compile invocations in one case. Its six response files are now checked by three cases of two. --- tools/tests/test_check_compile.py | 28 +++++++++++++++++-------- tools/tests/test_check_compile_modes.py | 4 +++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index e40969615c..5ae598662b 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -706,7 +706,25 @@ class CheckCompileResponseTests(CheckCompileFixture): response.write_bytes(content.encode("utf-8")) self.assertEqual(check_compile.expand_response_files(["@flags.rsp"], self.build), expected) - def test_response_options_compile_without_changing_build_outputs(self): + def test_empty_response_options_compile_without_changing_build_outputs(self): + self.check_response_option_outputs(( + ("", "int x;\n"), + (" \t\r\n", "int x;\n"), + )) + + def test_response_definitions_compile_without_changing_build_outputs(self): + self.check_response_option_outputs(( + (" -DA=7 -DB=11", "_Static_assert(A == 7 && B == 11, \"definitions\");\n"), + ("-DA=7\n-DB=11\n", "_Static_assert(A == 7 && B == 11, \"definitions\");\n"), + )) + + def test_crlf_and_quoted_response_definitions_compile_without_changing_build_outputs(self): + self.check_response_option_outputs(( + ("-DA=7\r\n-DB=11\r\n", "_Static_assert(A == 7 && B == 11, \"definitions\");\n"), + (r'-DNAME=\"hello\" -DB=11', '_Static_assert(sizeof(NAME) == 6 && B == 11, "quoted definition");\n'), + )) + + def check_response_option_outputs(self, cases): compiler = shutil.which("clang") or shutil.which("gcc") if compiler is None: self.skipTest("needs a GNU-compatible C compiler") @@ -718,14 +736,6 @@ class CheckCompileResponseTests(CheckCompileFixture): ("-c", source.as_posix(), "-MMD", "-MF", outputs[1].as_posix(), "-o", outputs[0].as_posix())), encoding="utf-8") arguments = [compiler, "@" + str(response), "@" + str(output_options)] - cases = ( - ("", "int x;\n"), - (" \t\r\n", "int x;\n"), - (" -DA=7 -DB=11", "_Static_assert(A == 7 && B == 11, \"definitions\");\n"), - ("-DA=7\n-DB=11\n", "_Static_assert(A == 7 && B == 11, \"definitions\");\n"), - ("-DA=7\r\n-DB=11\r\n", "_Static_assert(A == 7 && B == 11, \"definitions\");\n"), - (r'-DNAME=\"hello\" -DB=11', '_Static_assert(sizeof(NAME) == 6 && B == 11, "quoted definition");\n'), - ) for content, text in cases: source.write_text(text, encoding="utf-8") response.write_bytes(content.encode("utf-8")) diff --git a/tools/tests/test_check_compile_modes.py b/tools/tests/test_check_compile_modes.py index 02068e1f90..9fe0fd9a5c 100644 --- a/tools/tests/test_check_compile_modes.py +++ b/tools/tests/test_check_compile_modes.py @@ -205,7 +205,9 @@ class CheckCompileModeTests(unittest.TestCase): self.check_modes(((),), configure=configure) def test_library_directory_values_can_look_like_modes(self): - self.check_modes((("-L", "-E"),)) + def configure(root, build, source): + return ["-L", "-E"], "", "" + self.check_modes(((),), configure=configure) def test_forced_include_values_can_look_like_modes(self): def configure(root, build, source): From f685ee8720408febadd7f8d01aeb5e804d5244f6 Mon Sep 17 00:00:00 2001 From: p3nicillin <lcmcclellan12@gmail.com> Date: Sun, 25 Oct 2026 18:22:24 +0000 Subject: [PATCH 616/702] fix(http2): trust the system CA list for HTTPS requests Every libSceSsl context started without trust anchors, so a libSceHttp2 request without a verify callback failed the known-CA check. RetroArch's updater, PS5LM and Porpoise all use version-3 templates over https:// without one. libSceSsl now parses /<sandbox word>/common/cert/CA_LIST.cer, served by libkernel from the host trust store, once per process into its own pool, and SslTrustSystemRoots_nid_no_patch links those roots after the context's own authorities for one connection. The HTTP/2 transport calls it after creating each connection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceHttp2/Transport.cpp | 2 + core/libs/prx/libSceSsl/CMakeLists.txt | 2 +- core/libs/prx/libSceSsl/Export.cpp | 69 +++++++++++++++++++++++-- docs/dev/debt/libSceHttp2.md | 1 + docs/dev/debt/libSceSsl.md | 3 ++ 5 files changed, 73 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceHttp2/Transport.cpp b/core/libs/prx/libSceHttp2/Transport.cpp index ab7389458f..a8a9ac6b71 100644 --- a/core/libs/prx/libSceHttp2/Transport.cpp +++ b/core/libs/prx/libSceHttp2/Transport.cpp @@ -27,6 +27,7 @@ #include <thread> extern "C" int SslRead_nid_no_patch(int, void*, std::size_t, int); +extern "C" int SslTrustSystemRoots_nid_no_patch(int); namespace Http2 { namespace { @@ -359,6 +360,7 @@ struct Request::State { if (result < 0) return result; ssl = sceSslCreateConnection(sslContext, socket, url.host.c_str()); Require(ssl > 0, "sceHttp2: SSL context or connection unavailable"); + Require(SslTrustSystemRoots_nid_no_patch(ssl) == 0, "sceHttp2: system CA list unavailable"); Require(sceSslDisableVerifyOption(ssl, 0x3d) == 0 && sceSslEnableVerifyOption(ssl, settings.verifyFlags) == 0, "sceHttp2: SSL verification options rejected"); Require(sceSslSetAlpn(ssl, "h2") == 0, "sceHttp2: ALPN setup failed"); diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index cf5b24b014..ae58e01415 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -94,7 +94,7 @@ if(BUILD_TESTING) add_test(NAME guest_ssl_ps5_readers COMMAND guest_ssl_ps5_readers_tests) add_test_executable(guest_ssl_context_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/SslContextLifetime.cpp) target_include_directories(guest_ssl_context_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_ssl_context_lifetime_tests PRIVATE libc mbedtls mbedx509 mbedcrypto) + target_link_libraries(guest_ssl_context_lifetime_tests PRIVATE libkernel libc mbedtls mbedx509 mbedcrypto) configure_windows_unwind(guest_ssl_context_lifetime_tests) add_test(NAME guest_ssl_context_lifetime COMMAND guest_ssl_context_lifetime_tests) set_tests_properties(guest_ssl_context_lifetime PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index c8a56dc09d..79007fbb9f 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -27,6 +27,7 @@ #include <mbedtls/x509_crt.h> #include <algorithm> +#include <array> #include <climits> #include <cstdlib> #include <cstring> @@ -39,6 +40,13 @@ #include <utility> #include <vector> +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes); +int APS5_VABI sceKernelClose(int d); +const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); +} + namespace { constexpr int ErrorNotFound = static_cast<int>(0x8095f004); @@ -185,6 +193,40 @@ struct CertificateChain { ~CertificateChain() { mbedtls_x509_crt_free(&root); } }; +Pool systemRootPool{std::numeric_limits<std::size_t>::max() / 2}; + +std::string ReadSystemCaList() { + const std::string path = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert/CA_LIST.cer"; + const int descriptor = sceKernelOpen(path.c_str(), 0, 0); + if (descriptor < 0) return {}; + std::string text; + std::array<char, 16384> buffer{}; + for (;;) { + const auto count = sceKernelRead(descriptor, buffer.data(), buffer.size()); + if (count < 0) { + sceKernelClose(descriptor); + throw std::runtime_error("SSL: reading the system CA list failed"); + } + if (count == 0) break; + text.append(buffer.data(), static_cast<std::size_t>(count)); + } + sceKernelClose(descriptor); + return text; +} + +const mbedtls_x509_crt* SystemRoots() { + static const std::unique_ptr<CertificateChain> roots = [] { + auto chain = std::make_unique<CertificateChain>(); + const auto text = ReadSystemCaList(); + if (text.empty()) return chain; + PoolScope scope(systemRootPool); + const int result = mbedtls_x509_crt_parse(&chain->root, reinterpret_cast<const unsigned char*>(text.c_str()), text.size() + 1); + if (result < 0) CheckBackend(result, "SSL system CA list parse failed"); + return chain; + }(); + return roots->root.raw.p ? &roots->root : nullptr; +} + struct PrivateKey { mbedtls_pk_context key; PrivateKey() { mbedtls_pk_init(&key); } @@ -317,6 +359,7 @@ struct Session { Random random; PrivateKey privateKey; mbedtls_x509_crt authorities; + mbedtls_x509_crt* linkedSystemRoots = nullptr; mbedtls_ssl_config config; mbedtls_ssl_context ssl; @@ -331,20 +374,29 @@ struct Session { PoolScope scope(context->pool); mbedtls_ssl_free(&ssl); mbedtls_ssl_config_free(&config); + if (linkedSystemRoots) linkedSystemRoots->next = nullptr; mbedtls_x509_crt_free(&authorities); } - void Configure(const std::string& host, const std::string& alpn, std::uint32_t options) { + void Configure(const std::string& host, const std::string& alpn, std::uint32_t options, const mbedtls_x509_crt* systemRoots = nullptr) { verifyOptions = options; random.Seed(); for (const auto& chain : context->authorities) for (const auto* entry = &chain->root; entry; entry = entry->next) CheckBackend(mbedtls_x509_crt_parse_der(&authorities, entry->raw.p, entry->raw.len), "SSL CA certificate copy failed"); + auto* trusted = &authorities; + if (systemRoots && !authorities.raw.p) { + trusted = const_cast<mbedtls_x509_crt*>(systemRoots); + } else if (systemRoots) { + linkedSystemRoots = &authorities; + while (linkedSystemRoots->next) linkedSystemRoots = linkedSystemRoots->next; + linkedSystemRoots->next = const_cast<mbedtls_x509_crt*>(systemRoots); + } CheckBackend(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT), "SSL client configuration failed"); mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_OPTIONAL); mbedtls_ssl_conf_min_tls_version(&config, MBEDTLS_SSL_VERSION_TLS1_2); - mbedtls_ssl_conf_ca_chain(&config, &authorities, nullptr); + mbedtls_ssl_conf_ca_chain(&config, trusted, nullptr); mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &random.drbg); if (identity) { if (!identity->chain || !identity->key) throw std::logic_error("Incomplete SSL client identity is not supported"); @@ -480,6 +532,7 @@ struct SslConnection { int minimumVersion = SslVersionTls12; std::string alpn; std::shared_ptr<Session> session; + bool systemRoots = false; }; struct Runtime { @@ -834,6 +887,16 @@ int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) return id; } +int SslTrustSystemRoots_nid_no_patch(int sslConnectionId) { + SystemRoots(); + std::lock_guard lock(State().mutex); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (connection->session) throw std::logic_error("SslTrustSystemRoots_nid_no_patch: the connection has already started"); + connection->systemRoots = true; + return 0; +} + int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { std::shared_ptr<Session> session; std::lock_guard lock(State().mutex); @@ -882,7 +945,7 @@ int Establish(const char* function, int sslConnectionId, std::shared_ptr<Session if (!KernelLookupSocketNative_nid_no_patch(connection->socket, &owner, &native)) return ErrorNetBadDescriptor; PoolScope scope(context->pool); session = std::make_shared<Session>(connection->context, std::move(context), std::move(owner), native); - session->Configure(connection->host, connection->alpn, connection->verifyOptions); + session->Configure(connection->host, connection->alpn, connection->verifyOptions, connection->systemRoots ? SystemRoots() : nullptr); connection->session = session; } else { session = connection->session; diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index f89a6dac77..5e9882dea4 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -13,6 +13,7 @@ - [libSceHttp2 pending exports](../../../core/libs/prx/libSceHttp2/Export.cpp) - 5 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHttp2.c). ### Functional +- [Default TLS trust](../../../core/libs/prx/libSceHttp2/Transport.cpp) (libSceHttp2) - HTTPS connections also trust the system CA list, `/<sandbox word>/common/cert/CA_LIST.cer`, served by libkernel from the host trust store, after the title's own libSceSsl certificates. RetroArch (PPSA99169, [updater](https://github.com/mihawk-99/PS5_RetroArch/blob/7268510b12fe7461342e0cc4497c87a0910c1055/src/webui_update.cpp#L302-L312)), PS5LM (PPSA99575) and Porpoise (PPSA99764) send version-3 requests to public `https://` servers with no loaded certificate, verify callback or verification option, which pass the known-CA check only if the console trusts public roots by default. That the console's roots are exactly this file is not measured - [libSceHttp2 transport](../../../core/libs/prx/libSceHttp2/Transport.cpp) - only synchronous HTTPS requests with version 3 and ALPN `h2` are supported. HTTP/1.x, cleartext HTTP/2, negotiation fallback, connection reuse, multiplexing across requests, CONNECT, response trailers, automatic redirects and authentication challenges throw. Version values, method strings, header overwrite/append modes, status/read/content-length signatures and timeout units follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs); wire framing and negotiation follow [RFC 9113 sections 3, 5, 6 and 8](https://www.rfc-editor.org/rfc/rfc9113.html). [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp) instead records the method argument as an integer; the string signature follows SharpProspero. These are public compatibility contracts, not console measurements. - [libSceHttp2 error paths](../../../core/libs/prx/libSceHttp2/Transport.cpp) - cancellation, deadlines, DNS/TLS/protocol failures, malformed inputs, concurrent calls on one request and exhausted connection slots throw because their console error mappings are unknown. Socket connect errors propagate the libSceNet result, following the module-analysis report in [prosper #2894](https://github.com/mattias800/prosper/issues/2894); the report does not establish every exported error path. A request uses one connection; a context's concurrency cap counts connections until EOF, failure or deletion. Native pooling, scheduling, validation order and teardown semantics remain unverified. DNS uses two host workers and a queue of eight; cancellation/deadlines abandon the caller's wait, not libc's lookup. Library unload can wait for a host lookup to return. diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index cbd359521e..ecb2c1a0ee 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -14,3 +14,6 @@ - [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp): native argument validation, CRLF line endings, 64-character Base64 lines, NUL-inclusive length and buffer clearing on insufficient capacity are recovered. Foreign and expired certificate handles throw instead of performing native unchecked pointer access. DER reserialization differences for noncanonical certificates remain unverified. - This pending declaration has an unverified signature and behavior; it is a parameterless import-resolution declaration and throws through `NotImplemented_nid_no_patch`: [`sceSslSetServerName`](../../../core/libs/prx/libSceSsl/Export.cpp). + +### Functional +- [System roots for HTTP](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - `SslTrustSystemRoots_nid_no_patch` makes one connection also trust the system CA list, read once per process from `/<sandbox word>/common/cert/CA_LIST.cer`. It is parsed into a separate allocation pool, not charged to the title's `sceSslInit` pool, and linked after the context's own authorities; `sceSslGetCaList` and `sceSslGetCaCerts` do not report it. Only the HTTP libraries call it: whether a bare libSceSsl connection on the console trusts the system roots is not measured, so `sceSslCreateConnection` alone keeps trusting only the context's authorities From 509629ae90d078213af4f6a3696d72dc98d14a05 Mon Sep 17 00:00:00 2001 From: p3nicillin <lcmcclellan12@gmail.com> Date: Mon, 26 Oct 2026 12:27:53 +0000 Subject: [PATCH 617/702] fix(libSceSsl): refuse a missing system CA list instead of trusting nothing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceSsl/Export.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 79007fbb9f..36ee376faf 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -196,9 +196,11 @@ struct CertificateChain { Pool systemRootPool{std::numeric_limits<std::size_t>::max() / 2}; std::string ReadSystemCaList() { - const std::string path = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert/CA_LIST.cer"; + const char* const word = sceKernelGetFsSandboxRandomWord(); + if (word == nullptr) throw std::runtime_error("SSL: the sandbox word for the system CA list is not available"); + const std::string path = std::string("/") + word + "/common/cert/CA_LIST.cer"; const int descriptor = sceKernelOpen(path.c_str(), 0, 0); - if (descriptor < 0) return {}; + if (descriptor < 0) throw std::runtime_error("SSL: cannot open the system CA list " + path); std::string text; std::array<char, 16384> buffer{}; for (;;) { From d0d27f6183515e4b03964bdf9b4c41543b96a6a9 Mon Sep 17 00:00:00 2001 From: p3nicillin <lcmcclellan12@gmail.com> Date: Tue, 27 Oct 2026 17:51:50 +0000 Subject: [PATCH 618/702] fix(libSceSsl): throw for an empty or certificate-free system CA list An empty list, or one with no certificate, made SystemRoots() return a null chain, so the connection trusted nothing. Throw and name the path, as the missing-file case does; nothing is cached, so a later call reads the list again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceSsl/Export.cpp | 17 +++++++++++------ docs/dev/debt/libSceSsl.md | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 36ee376faf..b47ae27995 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -195,10 +195,13 @@ struct CertificateChain { Pool systemRootPool{std::numeric_limits<std::size_t>::max() / 2}; -std::string ReadSystemCaList() { +std::string SystemCaListPath() { const char* const word = sceKernelGetFsSandboxRandomWord(); if (word == nullptr) throw std::runtime_error("SSL: the sandbox word for the system CA list is not available"); - const std::string path = std::string("/") + word + "/common/cert/CA_LIST.cer"; + return std::string("/") + word + "/common/cert/CA_LIST.cer"; +} + +std::string ReadSystemCaList(const std::string& path) { const int descriptor = sceKernelOpen(path.c_str(), 0, 0); if (descriptor < 0) throw std::runtime_error("SSL: cannot open the system CA list " + path); std::string text; @@ -218,15 +221,17 @@ std::string ReadSystemCaList() { const mbedtls_x509_crt* SystemRoots() { static const std::unique_ptr<CertificateChain> roots = [] { + const auto path = SystemCaListPath(); + const auto text = ReadSystemCaList(path); + if (text.empty()) throw std::runtime_error("SSL: the system CA list " + path + " is empty"); auto chain = std::make_unique<CertificateChain>(); - const auto text = ReadSystemCaList(); - if (text.empty()) return chain; PoolScope scope(systemRootPool); const int result = mbedtls_x509_crt_parse(&chain->root, reinterpret_cast<const unsigned char*>(text.c_str()), text.size() + 1); - if (result < 0) CheckBackend(result, "SSL system CA list parse failed"); + if (result < 0 && currentPoolExhausted) throw std::bad_alloc(); + if (result < 0 || !chain->root.raw.p) throw std::runtime_error("SSL: the system CA list " + path + " has no certificate"); return chain; }(); - return roots->root.raw.p ? &roots->root : nullptr; + return &roots->root; } struct PrivateKey { diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index ecb2c1a0ee..a45d758ec6 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -16,4 +16,4 @@ - This pending declaration has an unverified signature and behavior; it is a parameterless import-resolution declaration and throws through `NotImplemented_nid_no_patch`: [`sceSslSetServerName`](../../../core/libs/prx/libSceSsl/Export.cpp). ### Functional -- [System roots for HTTP](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - `SslTrustSystemRoots_nid_no_patch` makes one connection also trust the system CA list, read once per process from `/<sandbox word>/common/cert/CA_LIST.cer`. It is parsed into a separate allocation pool, not charged to the title's `sceSslInit` pool, and linked after the context's own authorities; `sceSslGetCaList` and `sceSslGetCaCerts` do not report it. Only the HTTP libraries call it: whether a bare libSceSsl connection on the console trusts the system roots is not measured, so `sceSslCreateConnection` alone keeps trusting only the context's authorities +- [System roots for HTTP](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - `SslTrustSystemRoots_nid_no_patch` makes one connection also trust the system CA list, read once per process from `/<sandbox word>/common/cert/CA_LIST.cer`. It is parsed into a separate allocation pool, not charged to the title's `sceSslInit` pool, and linked after the context's own authorities; `sceSslGetCaList` and `sceSslGetCaCerts` do not report it. A missing, empty or certificate-free list throws and names the path instead of trusting nothing. Only the HTTP libraries call it: whether a bare libSceSsl connection on the console trusts the system roots is not measured, so `sceSslCreateConnection` alone keeps trusting only the context's authorities From 6cb305e8443e3a9ae7ab78dd6270015e4466229b Mon Sep 17 00:00:00 2001 From: p3nicillin <lcmcclellan12@gmail.com> Date: Tue, 27 Oct 2026 17:51:51 +0000 Subject: [PATCH 619/702] test(libSceSsl): cover the system CA list helper and its HTTP2 call Each test serves its own CA_LIST.cer through the working-directory override of the sandbox path. guest_ssl_system_roots checks that a handshake to the fixture server fails without the helper and passes with it, that a context authority still works alongside the list, and that an empty or certificate-free list throws with its path. guest_http2_system_roots sends a request from a context with no loaded certificate, which passes only through the transport's helper call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceHttp2/CMakeLists.txt | 7 +- core/libs/prx/libSceSsl/CMakeLists.txt | 14 +- core/libs/tests/GuestHttp2Transport.cpp | 13 +- core/libs/tests/GuestSslSystemRoots.cpp | 206 +++++++++++++++++++++++ 4 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestSslSystemRoots.cpp diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt index de8b70d50c..51dfe47743 100644 --- a/core/libs/prx/libSceHttp2/CMakeLists.txt +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -52,12 +52,15 @@ if(BUILD_TESTING) configure_windows_unwind(guest_http2_transport_tests) add_test(NAME guest_http2_transport COMMAND guest_http2_transport_tests) add_test(NAME guest_http2_early_response COMMAND guest_http2_transport_tests --early) - set_tests_properties(guest_http2_transport guest_http2_early_response PROPERTIES TIMEOUT 30) + set(http2SystemRootsRun ${CMAKE_CURRENT_BINARY_DIR}/guest_http2_system_roots) + file(MAKE_DIRECTORY ${http2SystemRootsRun}) + add_test(NAME guest_http2_system_roots COMMAND guest_http2_transport_tests --system-roots WORKING_DIRECTORY ${http2SystemRootsRun}) + set_tests_properties(guest_http2_transport guest_http2_early_response guest_http2_system_roots PROPERTIES TIMEOUT 30) if(WIN32) target_link_libraries(guest_http2_transport_tests PRIVATE ws2_32 bcrypt) add_test(NAME guest_http2_dns_first COMMAND guest_http2_transport_tests --dns-first) set_tests_properties(guest_http2_dns_first PROPERTIES TIMEOUT 30) - set_tests_properties(guest_http2 guest_http2_cookies guest_http2_options guest_http2_transport guest_http2_early_response guest_http2_dns_first PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_http2 guest_http2_cookies guest_http2_options guest_http2_transport guest_http2_early_response guest_http2_dns_first guest_http2_system_roots PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp2>") endif() endif() diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index ae58e01415..bd7f27b9f7 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -87,6 +87,16 @@ if(BUILD_TESTING) add_test(NAME guest_ssl_transport_${table} COMMAND guest_ssl_transport_tests ${table}) set_tests_properties(guest_ssl_transport_${table} PROPERTIES TIMEOUT 30) endforeach() + add_test_executable(guest_ssl_system_roots_tests ${LIBS_INCLUDE_DIR}/tests/GuestSslSystemRoots.cpp) + target_include_directories(guest_ssl_system_roots_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ssl_system_roots_tests PRIVATE libSceSsl libkernel libc mbedtls mbedx509 mbedcrypto) + configure_windows_unwind(guest_ssl_system_roots_tests) + foreach(list IN ITEMS system context empty blank) + set(sslSystemRootsRun ${CMAKE_CURRENT_BINARY_DIR}/guest_ssl_system_roots_${list}) + file(MAKE_DIRECTORY ${sslSystemRootsRun}) + add_test(NAME guest_ssl_system_roots_${list} COMMAND guest_ssl_system_roots_tests ${list} WORKING_DIRECTORY ${sslSystemRootsRun}) + set_tests_properties(guest_ssl_system_roots_${list} PROPERTIES TIMEOUT 30) + endforeach() add_test_executable(guest_ssl_ps5_readers_tests ${LIBS_INCLUDE_DIR}/tests/GuestSslPs5Readers.cpp) target_include_directories(guest_ssl_ps5_readers_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ssl_ps5_readers_tests PRIVATE libSceSsl libc) @@ -101,7 +111,9 @@ if(BUILD_TESTING) if(WIN32) target_link_libraries(guest_ssl_transport_tests PRIVATE bcrypt ws2_32) target_link_libraries(guest_ssl_context_lifetime_tests PRIVATE bcrypt ws2_32) - set_tests_properties(guest_ssl guest_ssl_load guest_ssl_ps5_readers guest_ssl_context_lifetime guest_ssl_transport_kernel guest_ssl_transport_net PROPERTIES ENVIRONMENT_MODIFICATION + target_link_libraries(guest_ssl_system_roots_tests PRIVATE bcrypt ws2_32) + set_tests_properties(guest_ssl guest_ssl_load guest_ssl_ps5_readers guest_ssl_context_lifetime guest_ssl_transport_kernel guest_ssl_transport_net + guest_ssl_system_roots_system guest_ssl_system_roots_context guest_ssl_system_roots_empty guest_ssl_system_roots_blank PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSsl>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNet>") endif() endif() diff --git a/core/libs/tests/GuestHttp2Transport.cpp b/core/libs/tests/GuestHttp2Transport.cpp index b785722d08..03594e78b6 100644 --- a/core/libs/tests/GuestHttp2Transport.cpp +++ b/core/libs/tests/GuestHttp2Transport.cpp @@ -8,6 +8,7 @@ #endif #include "SceTypes.hpp" +#include "prx/libkernel/include/Api.hpp" #include "prx/libSceHttp2/include/Http2.hpp" #include "prx/libSceNet/include/Net.hpp" #include "prx/libSceSsl/include/Ssl.hpp" @@ -28,6 +29,8 @@ #include <cstdio> #include <cstdlib> #include <cstring> +#include <filesystem> +#include <fstream> #include <future> #include <map> #include <mutex> @@ -526,7 +529,15 @@ int main(int argc, char** argv) { SceSsl::Data root{reinterpret_cast<std::uint8_t*>(const_cast<char*>(RootPem)), sizeof(RootPem)}; SceSsl::Data* authorities[] = {&root}; Require(sceSslLoadCert(ssl, 1, authorities, nullptr, nullptr) == 0, "trust loopback CA"); - if (argc == 2 && std::string_view(argv[1]) == "--early") { + if (argc == 2 && std::string_view(argv[1]) == "--system-roots") { + const auto list = std::filesystem::current_path() / sceKernelGetFsSandboxRandomWord() / "common" / "cert" / "CA_LIST.cer"; + std::filesystem::create_directories(list.parent_path()); + std::ofstream(list, std::ios::binary | std::ios::trunc) << RootPem; + const int unloaded = sceSslInit_nid_postfix(8 * 1024 * 1024); + Require(unloaded > 0, "SSL context without certificates"); + Success(unloaded, Mode::Normal); + Require(sceSslTerm_nid_postfix(unloaded) == 0, "SSL context without certificates term"); + } else if (argc == 2 && std::string_view(argv[1]) == "--early") { for (const auto mode : {Mode::EarlyFinal, Mode::EarlyReset, Mode::EarlyEmpty, Mode::EarlyLarge}) EarlyResponse(ssl, mode); Failure(ssl, Mode::EarlyResetIncomplete); #ifdef _WIN32 diff --git a/core/libs/tests/GuestSslSystemRoots.cpp b/core/libs/tests/GuestSslSystemRoots.cpp new file mode 100644 index 0000000000..915f98b32e --- /dev/null +++ b/core/libs/tests/GuestSslSystemRoots.cpp @@ -0,0 +1,206 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libSceSsl/include/Ssl.hpp" +#ifdef _WIN32 +#include <winsock2.h> +#else +#include <netinet/in.h> +#include <sys/socket.h> +#include <csignal> +#endif + +#include "prx/libSceSsl/SslTypes.hpp" +#include "SslCertificateFixture.hpp" + +#include <mbedtls/ctr_drbg.h> +#include <mbedtls/entropy.h> +#include <mbedtls/net_sockets.h> +#include <mbedtls/pk.h> +#include <mbedtls/ssl.h> +#include <mbedtls/x509_crt.h> + +#include <array> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <exception> +#include <filesystem> +#include <fstream> +#include <string> +#include <thread> + +extern "C" int SslTrustSystemRoots_nid_no_patch(int sslConnectionId); + +namespace { + +constexpr int ErrorInvalidId = static_cast<int>(0x8095f006); +constexpr int ErrorUnknownCa = static_cast<int>(0x8095f00c); + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::exit(1); +} + +std::string ThrownMessage(int connection) { + try { + SslTrustSystemRoots_nid_no_patch(connection); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +void ServeSystemList(const std::string& text) { + const auto path = std::filesystem::current_path() / sceKernelGetFsSandboxRandomWord() / "common" / "cert" / "CA_LIST.cer"; + std::filesystem::create_directories(path.parent_path()); + std::ofstream file(path, std::ios::binary | std::ios::trunc); + file << text; + Require(static_cast<bool>(file.flush()), "write the system CA list"); +} + +struct Server { + mbedtls_net_context listener; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context drbg; + mbedtls_x509_crt certificate; + mbedtls_pk_context key; + mbedtls_ssl_config config; + std::uint16_t port = 0; + + Server() { + mbedtls_net_init(&listener); + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&drbg); + mbedtls_x509_crt_init(&certificate); + mbedtls_pk_init(&key); + mbedtls_ssl_config_init(&config); + Require(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0) == 0, "seed server random generator"); + Require(mbedtls_x509_crt_parse(&certificate, reinterpret_cast<const unsigned char*>(ServerPem), sizeof(ServerPem)) == 0, + "parse server certificate"); + Require(mbedtls_pk_parse_key(&key, reinterpret_cast<const unsigned char*>(ServerKey), sizeof(ServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse server key"); + Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, + "configure server"); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); + Require(mbedtls_ssl_conf_own_cert(&config, &certificate, &key) == 0, "set server certificate"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_NONE); + Require(mbedtls_net_bind(&listener, "127.0.0.1", "0", MBEDTLS_NET_PROTO_TCP) == 0, "bind loopback listener"); + sockaddr_in address{}; +#ifdef _WIN32 + int size = sizeof(address); +#else + socklen_t size = sizeof(address); +#endif + Require(getsockname(listener.fd, reinterpret_cast<sockaddr*>(&address), &size) == 0, "read listener port"); + port = ntohs(address.sin_port); + } + + ~Server() { + mbedtls_ssl_config_free(&config); + mbedtls_pk_free(&key); + mbedtls_x509_crt_free(&certificate); + mbedtls_ctr_drbg_free(&drbg); + mbedtls_entropy_free(&entropy); + mbedtls_net_free(&listener); + } + + void Serve() { + mbedtls_net_context client; + mbedtls_net_init(&client); + mbedtls_ssl_context ssl; + mbedtls_ssl_init(&ssl); + if (mbedtls_net_accept(&listener, &client, nullptr, 0, nullptr) == 0 && mbedtls_ssl_setup(&ssl, &config) == 0) { + mbedtls_ssl_set_bio(&ssl, &client, mbedtls_net_send, mbedtls_net_recv, nullptr); + if (mbedtls_ssl_handshake(&ssl) == 0) mbedtls_ssl_close_notify(&ssl); + } + mbedtls_ssl_free(&ssl); + mbedtls_net_free(&client); + } +}; + +int LoadAuthority(const char* pem) { + const int context = sceSslInit_nid_postfix(256 * 1024); + Require(context > 0, "create an SSL context"); + std::string text = pem; + SceSsl::Data authority{reinterpret_cast<std::uint8_t*>(text.data()), text.size()}; + auto* authorities = &authority; + Require(sceSslLoadCert(context, 1, &authorities, nullptr, nullptr) == 0, "load the context authority"); + return context; +} + +int Connect(int context, std::uint16_t port, bool systemRoots) { + const int descriptor = socket_nid_postfix(2, 1, 6); + Require(descriptor >= 0, "create guest socket"); + std::array<std::uint8_t, 16> address{16, 2, static_cast<std::uint8_t>(port >> 8), static_cast<std::uint8_t>(port), 127, 0, 0, 1}; + Require(connect_nid_postfix(descriptor, address.data(), 16) == 0, "connect guest socket"); + const int connection = sceSslCreateConnection(context, descriptor, "localhost"); + Require(connection > 0, "create an SSL connection"); + if (systemRoots) Require(SslTrustSystemRoots_nid_no_patch(connection) == 0, "trust the system CA list"); + const int result = sceSslConnect(connection); + if (result == 0) { + Require(!ThrownMessage(connection).empty(), "the system CA list was added to a started connection"); + std::array<char, 4> scratch{}; + Require(sceSslRead(connection, scratch.data(), scratch.size(), 0) == 0, "close notify did not end the stream"); + } + Require(sceSslDeleteConnection(connection) == 0 && close_nid_postfix(descriptor) == 0, "release the connection"); + return result; +} + +void CheckRejectedList(const std::string& text, const char* message) { + ServeSystemList(text); + const int context = sceSslInit_nid_postfix(256 * 1024); + Require(context > 0, "create an SSL context"); + const int connection = sceSslCreateConnection(context, 1000, "localhost"); + Require(connection > 0, "create an SSL connection"); + const auto path = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert/CA_LIST.cer"; + Require(ThrownMessage(connection).find(path) != std::string::npos, message); + Require(ThrownMessage(connection).find(path) != std::string::npos, "a rejected system CA list was cached"); + Require(sceSslDeleteConnection(connection) == 0 && sceSslTerm_nid_postfix(context) == 0, "release the context"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "usage: system|context|empty|blank"); +#ifndef _WIN32 + std::signal(SIGPIPE, SIG_IGN); +#endif + const std::string mode = argv[1]; + if (mode == "empty") { + CheckRejectedList("", "an empty system CA list did not throw with its path"); + return 0; + } + if (mode == "blank") { + CheckRejectedList("no certificate here\n", "a system CA list without a certificate did not throw with its path"); + return 0; + } + Require(mode == "system" || mode == "context", "usage: system|context|empty|blank"); + ServeSystemList(mode == "system" ? RootPem : ClientPem); + Server server; + std::thread serving([&] { + for (int index = 0; index < 4; ++index) server.Serve(); + }); + const int empty = sceSslInit_nid_postfix(256 * 1024); + Require(empty > 0, "create an SSL context without authorities"); + Require(SslTrustSystemRoots_nid_no_patch(9999) == ErrorInvalidId, "an unknown connection id was accepted"); + if (mode == "system") { + Require(Connect(empty, server.port, false) == ErrorUnknownCa, "a connection without the system CA list trusted the fixture root"); + Require(Connect(empty, server.port, true) == 0, "the system CA list did not trust the fixture root"); + const int unrelated = LoadAuthority(ClientPem); + Require(Connect(unrelated, server.port, false) == ErrorUnknownCa, "an unrelated context authority trusted the server"); + Require(Connect(unrelated, server.port, true) == 0, "the system CA list was not trusted after the context's authorities"); + Require(sceSslTerm_nid_postfix(unrelated) == 0, "release the context with an unrelated authority"); + } else { + Require(Connect(empty, server.port, true) == ErrorUnknownCa, "a system CA list without the fixture root trusted the server"); + const int fixture = LoadAuthority(RootPem); + Require(Connect(fixture, server.port, false) == 0, "the context's fixture root was not trusted"); + Require(Connect(fixture, server.port, true) == 0, "the context's fixture root was not trusted alongside the system CA list"); + Require(Connect(empty, server.port, false) == ErrorUnknownCa, "a connection without authorities trusted the server"); + Require(sceSslTerm_nid_postfix(fixture) == 0, "release the context with the fixture root"); + } + serving.join(); + Require(sceSslTerm_nid_postfix(empty) == 0, "release the context without authorities"); + std::puts("SSL system roots passed"); + return 0; +} From c38629b1596d7922b6b5b3a9a253bad60ba66107 Mon Sep 17 00:00:00 2001 From: p3nicillin <lcmcclellan12@gmail.com> Date: Tue, 27 Oct 2026 23:02:59 +0000 Subject: [PATCH 620/702] refactor(libSceSsl): take the kernel file calls from libkernel's headers Include File.hpp for sceKernelOpen, sceKernelRead and sceKernelClose and Api.hpp for sceKernelGetFsSandboxRandomWord instead of redeclaring them in a local extern "C" block, so each export keeps one declaration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceSsl/Export.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index b47ae27995..bae126e949 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -9,7 +9,9 @@ #include "SslTypes.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include "prx/libkernel/include/Api.hpp" #include <mbedtls/asn1.h> #include <mbedtls/base64.h> @@ -40,13 +42,6 @@ #include <utility> #include <vector> -extern "C" { -int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); -std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes); -int APS5_VABI sceKernelClose(int d); -const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); -} - namespace { constexpr int ErrorNotFound = static_cast<int>(0x8095f004); From e17983e34141b92aa2b30cf93e7e6b81b9c9f49d Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Tue, 27 Oct 2026 18:17:13 -0500 Subject: [PATCH 621/702] fix(agc): start no collect epoch at a wait whose own queue's label already reached memory waitMemory skipped the collect epoch bump for a WAIT_REG_MEM satisfied by a label its own queue stored after the wait was received only while that label was still pending. When the label had already reached memory, the at-entry return and the poll loop exit bumped anyway, so whether the wait was an ordering point depended on whether the GPU finished before the queue worker reached the wait. On the Linux CI runner (llvmpipe) the spin dispatch of agc_driver_same_queue_label_wait sometimes finished first and the test failed with 8 to 12 extra bumps for 16 waits. Both memory-satisfied returns now consult the label-store history the same way. The test gains a gated submission in which every label is in memory before its wait. --- .../src/Driver/Synchronization/WaitMemory.cpp | 6 +++ .../tests/execution/SameQueueLabelWait.cpp | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+) 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 f1b7df1757..99edca9a11 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp @@ -135,9 +135,14 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que if (bump) bumpEpoch(&EpochBumps::waits); } } epochPoint; + const auto storedByOwnLabel = [&] { + std::uint32_t writer = queue; + return !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer) && writer == queue; + }; GuestMemory::CheckRange(reinterpret_cast<const void*>(awaited), awaitedBytes, awaitedBytes); if (Pm4::WaitSatisfiedUnchecked(packet)) { ++outcomes.atEntry; + if (storedByOwnLabel()) epochPoint.bump = false; return; } if (heldAtSubmit) return; @@ -406,6 +411,7 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que } } ++outcomes.polled; + if (storedByOwnLabel()) epochPoint.bump = false; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp index 607faa3dd3..239b0d7cf6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp @@ -39,6 +39,7 @@ struct Slot { alignas(256) std::array<std::uint32_t, 4> spinDescriptor{}; alignas(256) volatile std::uint32_t label = 0; alignas(256) volatile std::uint32_t done = 0; + alignas(256) volatile std::uint32_t gate = 0; }; class GuestBlock { @@ -131,6 +132,35 @@ std::uint64_t Run(Slot& slot, std::uint32_t value, bool wait) { return AgcDriver::GuestMemory::CollectEpochBumps() - before; } +std::uint64_t RunLanded(Slot& slot, std::uint32_t value, bool wait) { + slot.done = 0; + slot.gate = 0; + std::vector<std::uint32_t> words; + for (std::uint32_t index = 0; index < Waits; ++index) { + ReleaseLabel(words, &slot.label, value + index); + WaitEqual(words, &slot.gate, index + 1); + if (wait) WaitEqual(words, &slot.label, value + index); + } + WriteData(words, &slot.done, 1); + const auto before = AgcDriver::GuestMemory::CollectEpochBumps(); + Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "gated compute submit failed"); + const auto start = std::chrono::steady_clock::now(); + for (std::uint32_t index = 0; index < Waits; ++index) { + while (slot.label != value + index) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), "label " + std::to_string(index) + " of the gated submission did not reach memory"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + slot.gate = index + 1; + } + while (slot.done == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), "the gated submission did not finish"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::GuestMemory::CollectEpochBumps() - before; +} + } int main() { @@ -149,6 +179,14 @@ int main() { extra = std::min(extra, with > without ? with - without : 0); } Require(extra < Waits / 2, "WAIT_REG_MEMs satisfied by labels their own queue released earlier in the submission started new collect epochs: " + std::to_string(extra) + " extra bumps for " + std::to_string(Waits) + " waits"); + std::uint64_t landedExtra = ~std::uint64_t{0}; + for (std::uint32_t attempt = 0; attempt < 3; ++attempt) { + const auto without = RunLanded(slot, 1000 + attempt * 2 * Waits, false); + const auto with = RunLanded(slot, 1000 + (attempt * 2 + 1) * Waits, true); + std::cout << "landed attempt " << attempt << ": " << with << " bumps with " << Waits << " waits, " << without << " without\n"; + landedExtra = std::min(landedExtra, with > without ? with - without : 0); + } + Require(landedExtra < Waits / 2, "WAIT_REG_MEMs that found the labels their own queue released earlier in the submission already in memory started new collect epochs: " + std::to_string(landedExtra) + " extra bumps for " + std::to_string(Waits) + " waits"); std::cout << "same-queue label wait test passed\n"; AgcDriverShutdown_nid_postfix(); return 0; From e9de93c1e426f412ab42d5fa4f0d9c024c9904c1 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Tue, 27 Oct 2026 23:25:36 +0000 Subject: [PATCH 622/702] docs(libSceNpEntitlementAccess): list sceNpEntitlementAccessInitialize as a silent stub Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceNpEntitlementAccess.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/debt/libSceNpEntitlementAccess.md b/docs/dev/debt/libSceNpEntitlementAccess.md index 7ce2b8189c..6baf5c27d3 100644 --- a/docs/dev/debt/libSceNpEntitlementAccess.md +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -1,5 +1,9 @@ # libSceNpEntitlementAccess technical debt +### Silent stubs + +- [sceNpEntitlementAccessInitialize](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - returns 0 without reading the init parameter or writing the boot parameter, so the boot parameter keeps what the caller put there. Both are 32 reserved bytes here, and PPSA01494 (at both of its calls), PPSA02225, PPSA03984, PPSA30839, PPSA31162 and PPSA34502 zero exactly 32 bytes of each right before the call. PPSA03984 and PPSA31162 test the result for a negative value. What the console writes into the boot parameter is unknown + ### Unknown function info - [sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual, sceNpEntitlementAccessPollConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumeEntitlement](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpEntitlementAccess.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim From 033d1d611046a211bbc8b026a6a040457071588d Mon Sep 17 00:00:00 2001 From: Omar Elmahy <elma7e@ieee.org> Date: Tue, 27 Oct 2026 23:30:34 +0000 Subject: [PATCH 623/702] test(libSceZlib): cover uninitialized precedence and post-finalization returns --- core/libs/tests/GuestZlib.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 15ad0686d0..81586998dd 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -34,6 +34,9 @@ int main() { std::int32_t status = 0; Require(sceZlibFinalize() == static_cast<std::int32_t>(0x81120032)); Require(sceZlibInflate(nullptr, 0, nullptr, 0, nullptr) == static_cast<std::int32_t>(0x81120032)); + Require(sceZlibWaitForDone(nullptr, nullptr) == static_cast<std::int32_t>(0x81120032)); + std::uint64_t earlyId = 0; + Require(sceZlibWaitForDone(&earlyId, nullptr) == static_cast<std::int32_t>(0x81120032)); Require(sceZlibInitialize(nullptr, 1) == static_cast<std::int32_t>(0x81120016)); Require(sceZlibInitialize(output, 0) == static_cast<std::int32_t>(0x81120016)); Require(sceZlibInitialize(nullptr, 0) == 0); @@ -145,4 +148,7 @@ int main() { Require(sceZlibInitialize(nullptr, 0) == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast<std::int32_t>(0x81120002)); Require(sceZlibFinalize() == 0); + Require(sceZlibFinalize() == static_cast<std::int32_t>(0x81120032)); + Require(sceZlibInflate(nullptr, 0, nullptr, 0, nullptr) == static_cast<std::int32_t>(0x81120032)); + Require(sceZlibWaitForDone(nullptr, nullptr) == static_cast<std::int32_t>(0x81120032)); } From 2db362915ce29ffe900fabc6271e5642e4ffeb4f Mon Sep 17 00:00:00 2001 From: Omar Elmahy <elma7e@ieee.org> Date: Tue, 27 Oct 2026 23:34:05 +0000 Subject: [PATCH 624/702] test(libSceUsbStorage): cover unregister event bounds and multiple null arguments --- core/libs/tests/GuestUsbStorage.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/tests/GuestUsbStorage.cpp b/core/libs/tests/GuestUsbStorage.cpp index 4013c5af5f..8732ce9e14 100644 --- a/core/libs/tests/GuestUsbStorage.cpp +++ b/core/libs/tests/GuestUsbStorage.cpp @@ -279,6 +279,8 @@ void TestRegisterCallbacks() { "register a registered callback again did not throw"); Require(sceUsbStorageUnregisterCallback(1, nullptr) == UsbStorage::InvalidArgument, "unregister a null callback"); Require(sceUsbStorageUnregisterCallback(0, Callback()) == UsbStorage::InvalidArgument, "unregister callback of event type 0"); + Require(sceUsbStorageUnregisterCallback(3, Callback()) == UsbStorage::InvalidArgument, "unregister callback of event type 3"); + Require(sceUsbStorageUnregisterCallback(-1, Callback()) == UsbStorage::InvalidArgument, "unregister callback of a negative event type"); Require(sceUsbStorageUnregisterCallback(1, Callback()) == 0, "unregister the mount callback"); Require(sceUsbStorageUnregisterCallback(2, Callback()) == 0, "unregister the unmount callback"); Require(ThrowsWith([] { sceUsbStorageUnregisterCallback(1, Callback()); }, "sceUsbStorageUnregisterCallback not implemented"), @@ -309,6 +311,7 @@ void TestNoDevice() { Require(devices[0] == 0x5a5a5a5a && devices[3] == 0x5a5a5a5a, "the device list was written"); Require(sceUsbStorageGetDeviceList(nullptr, &count) == UsbStorage::InvalidArgument, "get the device list without a device array"); Require(sceUsbStorageGetDeviceList(devices, nullptr) == UsbStorage::InvalidArgument, "get the device list without a count"); + Require(sceUsbStorageGetDeviceList(nullptr, nullptr) == UsbStorage::InvalidArgument, "get device list without buffers"); UsbStorageDeviceInfo info; std::memset(&info, 0x5a, sizeof(info)); @@ -355,6 +358,7 @@ void TestGetdents() { Require(fd == -1 && bufferSize == 0x5a5a5a5a5a5a5a5aull, "the getdents open wrote its outputs"); Require(sceUsbStorageGetdentsOpen(0, nullptr, &bufferSize) == UsbStorage::InvalidArgument, "open a getdents scan without a descriptor"); Require(sceUsbStorageGetdentsOpen(0, &fd, nullptr) == UsbStorage::InvalidArgument, "open a getdents scan without a buffer size"); + Require(sceUsbStorageGetdentsOpen(0, nullptr, nullptr) == UsbStorage::InvalidArgument, "open getdents scan without descriptor and buffer size"); std::uint8_t buffer[64]; std::uint64_t bytesRead = 0x5a5a5a5a5a5a5a5aull; @@ -364,6 +368,7 @@ void TestGetdents() { Require(buffer[0] == 0x5a, "the getdents read wrote the buffer"); Require(sceUsbStorageGetdentsRead(1, nullptr, sizeof(buffer), &bytesRead) == UsbStorage::InvalidArgument, "read a getdents scan without a buffer"); Require(sceUsbStorageGetdentsRead(1, buffer, sizeof(buffer), nullptr) == UsbStorage::InvalidArgument, "read a getdents scan without a byte count"); + Require(sceUsbStorageGetdentsRead(1, nullptr, sizeof(buffer), nullptr) == UsbStorage::InvalidArgument, "read getdents scan without buffer and byte count"); Require(ThrowsWith([] { sceUsbStorageGetdentsClose(1); }, "sceUsbStorageGetdentsClose not implemented"), "close a getdents scan that was never opened did not throw its marker"); From 53621f808424cb1b4226e7b6709d487ace06b80c Mon Sep 17 00:00:00 2001 From: Omar Elmahy <elma7e@ieee.org> Date: Tue, 27 Oct 2026 23:50:41 +0000 Subject: [PATCH 625/702] test(libSceHmdSetupDialog): cover post-terminate lifecycle and argument precedence --- core/libs/tests/GuestHmdSetupDialog.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestHmdSetupDialog.cpp b/core/libs/tests/GuestHmdSetupDialog.cpp index 347f8c4ad1..af83eb7cc9 100644 --- a/core/libs/tests/GuestHmdSetupDialog.cpp +++ b/core/libs/tests/GuestHmdSetupDialog.cpp @@ -64,4 +64,12 @@ int main() { Require(sceHmdSetupDialogGetResult(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceHmdSetupDialogTerminate() == 0); + Require(sceHmdSetupDialogTerminate() == kErrNotInitialized); + Require(sceHmdSetupDialogOpen(nullptr) == kErrNotInitialized); + Require(sceHmdSetupDialogOpen(¶m) == kErrNotInitialized); + Require(sceHmdSetupDialogClose() == kErrNotInitialized); + Require(sceHmdSetupDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceHmdSetupDialogGetResult(result) == kErrNotInitialized); + Require(sceHmdSetupDialogGetStatus() == kStatusNone); + Require(sceHmdSetupDialogUpdateStatus() == kStatusNone); } From 9355599e374df4c54d681b461aeb73d0d24dcb3c Mon Sep 17 00:00:00 2001 From: Omar Elmahy <elma7e@ieee.org> Date: Tue, 27 Oct 2026 23:51:32 +0000 Subject: [PATCH 626/702] test(libSceSystemGesture): cover post-finalization entry point rejections --- core/libs/tests/GuestSystemGesture.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index 23970e8b82..54eb9642fd 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -277,5 +277,10 @@ int main() { for (std::size_t index = 1; index < otherHandles.size(); ++index) Require(sceSystemGestureClose(otherHandles[index]) == 0); Require(sceSystemGestureClose(handle) == 0); Require(sceSystemGestureFinalizePrimitiveTouchRecognizer() == 0); + Require(sceSystemGestureOpen(0, nullptr) == static_cast<int>(0x80890003)); + Require(sceSystemGestureClose(handle) == static_cast<int>(0x80890003)); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == static_cast<int>(0x80890003)); + Require(sceSystemGestureAppendTouchRecognizer(handle, &recognizer) == static_cast<int>(0x80890003)); + Require(sceSystemGestureFinalizePrimitiveTouchRecognizer() == 0); return 0; } From 4f7f483746c8b41728084ab942f6266d35ab2cd4 Mon Sep 17 00:00:00 2001 From: Omar Elmahy <elma7e@ieee.org> Date: Tue, 27 Oct 2026 23:52:06 +0000 Subject: [PATCH 627/702] test(libSceVrServiceDialog): cover post-terminate lifecycle and argument precedence --- core/libs/tests/GuestVrServiceDialog.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestVrServiceDialog.cpp b/core/libs/tests/GuestVrServiceDialog.cpp index 43664c8263..d37c208519 100644 --- a/core/libs/tests/GuestVrServiceDialog.cpp +++ b/core/libs/tests/GuestVrServiceDialog.cpp @@ -66,7 +66,10 @@ int main() { Require(sceVrServiceDialogTerminate() == 0); Require(sceVrServiceDialogTerminate() == kErrNotInitialized); Require(sceVrServiceDialogOpen(nullptr) == kErrNotInitialized); + Require(sceVrServiceDialogOpen(¶m) == kErrNotInitialized); + Require(sceVrServiceDialogClose() == kErrNotInitialized); Require(sceVrServiceDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceVrServiceDialogGetResult(result) == kErrNotInitialized); Require(sceVrServiceDialogGetStatus() == kStatusNone); Require(sceVrServiceDialogUpdateStatus() == kStatusNone); } From b34f675962120f6c92c42220e8a5fbcb22a169c1 Mon Sep 17 00:00:00 2001 From: Omar Elmahy <elma7e@ieee.org> Date: Tue, 27 Oct 2026 23:52:56 +0000 Subject: [PATCH 628/702] test(libSceSocialScreenDialog): cover post-terminate lifecycle and argument precedence --- core/libs/tests/GuestSocialScreenDialog.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestSocialScreenDialog.cpp b/core/libs/tests/GuestSocialScreenDialog.cpp index eb261f831e..c86884d5f1 100644 --- a/core/libs/tests/GuestSocialScreenDialog.cpp +++ b/core/libs/tests/GuestSocialScreenDialog.cpp @@ -66,7 +66,10 @@ int main() { Require(sceSocialScreenDialogTerminate() == 0); Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); Require(sceSocialScreenDialogOpen(nullptr) == kErrNotInitialized); + Require(sceSocialScreenDialogOpen(¶m) == kErrNotInitialized); + Require(sceSocialScreenDialogClose() == kErrNotInitialized); Require(sceSocialScreenDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceSocialScreenDialogGetResult(result) == kErrNotInitialized); Require(sceSocialScreenDialogGetStatus() == kStatusNone); Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); } From 609f4f1205e38c082e7561328de8bcdaa6f7233c Mon Sep 17 00:00:00 2001 From: QRMOE <217609482+QRMOE@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:08:00 +0100 Subject: [PATCH 629/702] test(agc): drop the written native depth buffer-write refusal case The case asserted that a valid but unimplemented guest publication of a written native depth throws. Tests keep to implemented behaviour, so it is removed; the unwritten depth-plane and HTILE cases remain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../tests/execution/NativeMultisampleDepth.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 8f652a790d..c8ca025ddc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -1120,7 +1120,7 @@ void CheckUnwrittenGuestAccess(Context context, VkFormat format, std::uint32_t f std::printf("Native depth format=%u samples=%u: guest access retired the unwritten surface and refused the written one\n", static_cast<std::uint32_t>(format), fragments); } -enum class BufferWrite { DepthPlane, Htile, Written }; +enum class BufferWrite { DepthPlane, Htile }; void CheckBufferWrite(Context context, VkFormat format, std::uint32_t fragments, BufferWrite write) { const VkExtent2D extent{5u, 3u}; @@ -1151,12 +1151,6 @@ void CheckBufferWrite(Context context, VkFormat format, std::uint32_t fragments, Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE && resident(), "native depth buffer-write fixture has no resident owner"); upload(unrelated.Address(), 256u); Require(resident(), "an unrelated buffer write retired a native depth surface"); - if (write == BufferWrite::Written) { - NoteDepthSurfaceWrite(context, target); - TargetRefuses([&] { upload(target.address, 256u); }, "guest publication"); - Require(resident(), "a refused buffer write retired a written native depth surface"); - return; - } if (write == BufferWrite::DepthPlane) upload(target.address + bytes - 256u, 256u); else upload(target.htileAddress, metadataBytes); Require(!resident(), "a buffer the GPU may write left an unwritten native depth surface resident over it"); @@ -1169,8 +1163,8 @@ void CheckBufferWrites(Context context, VkFormat format, std::uint32_t fragments std::printf("skipped native depth buffer writes: the device lacks VK_EXT_external_memory_host, VK_KHR_buffer_device_address or the bufferDeviceAddress feature\n"); return; } - for (const auto write : {BufferWrite::DepthPlane, BufferWrite::Htile, BufferWrite::Written}) CheckBufferWrite(context, format, fragments, write); - std::printf("Native depth format=%u samples=%u: a GPU buffer write over the depth or HTILE released the unwritten surface and refused the written one\n", static_cast<std::uint32_t>(format), fragments); + for (const auto write : {BufferWrite::DepthPlane, BufferWrite::Htile}) CheckBufferWrite(context, format, fragments, write); + std::printf("Native depth format=%u samples=%u: a GPU buffer write over the depth or HTILE released the unwritten surface\n", static_cast<std::uint32_t>(format), fragments); } void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { From 49d943b7d0eef7cc5e8c644a767a1df5a0682649 Mon Sep 17 00:00:00 2001 From: slopgobblercobbler <slopgobblercobbler@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:06:49 +0100 Subject: [PATCH 630/702] test(libSceSaveData.native): cover cancel and prepare for transferring unimplemented stubs --- core/libs/tests/GuestSaveDataCancel.cpp | 33 ++++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/core/libs/tests/GuestSaveDataCancel.cpp b/core/libs/tests/GuestSaveDataCancel.cpp index 5f9612d5ce..ed4a92ebaf 100644 --- a/core/libs/tests/GuestSaveDataCancel.cpp +++ b/core/libs/tests/GuestSaveDataCancel.cpp @@ -2,18 +2,33 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include <cstdio> #include <cstdint> +#include <cstdlib> #include <stdexcept> #include <string> -int main() { - try { - sceSaveDataCancel(0); +namespace { - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == "sceSaveDataCancel not implemented") return 0; - std::fprintf(stderr, "sceSaveDataCancel threw: %s\n", error.what()); - return 1; +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); } - std::fprintf(stderr, "sceSaveDataCancel returned instead of reporting that it is not implemented\n"); - return 1; +} + +template <typename TCall> +void RequireThrows(TCall call, const std::string& expectedMessage) { + try { + call(); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == expectedMessage, "unexpected error message"); + return; + } + Require(false, "call returned instead of reporting that it is not implemented"); +} + +} + +int main() { + RequireThrows([] { sceSaveDataCancel(0); }, "sceSaveDataCancel not implemented"); + return 0; } From ce51c9cbbc1a0722477e03619af962eb9a350b02 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:05:38 +0000 Subject: [PATCH 631/702] fix(shader): do not divide by zero control points when keying tessellated draws TessellationInstancePatches divided the draw's index count by the tessellation configuration's inputControlPoints. A request replayed from a serialized capture is not validated for that field, so a configuration with zero control points and an instanced draw raised SIGFPE while the cache key was built, before ShaderInputInfoBuilder could reject the configuration with its intended error. The function now reports no instance patches for zero control points and leaves the rejection to the builder. --- core/shader/recompiler/Recompiler.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 51a0b52768..c85d677c2a 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -458,7 +458,7 @@ struct RecompileRequest { }; inline std::uint32_t TessellationInstancePatches(const RecompileRequest& request) { - if (!request.graphics || !request.graphics->tessellation || request.graphics->draw.instanceCount <= 1u || request.graphics->tessellation->breakAtInstance) return 0u; + if (!request.graphics || !request.graphics->tessellation || request.graphics->draw.instanceCount <= 1u || request.graphics->tessellation->breakAtInstance || request.graphics->tessellation->inputControlPoints == 0u) return 0u; return request.graphics->draw.indexCount / request.graphics->tessellation->inputControlPoints; } From 59fef11858b83aa7482c5474c6915690c6123c02 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:05:38 +0000 Subject: [PATCH 632/702] test(shader): key a tessellated instanced draw with zero control points The instance patch count has to be zero instead of crashing, and a regular configuration still divides the index count by the control points. --- .../tests/RequestSerializerTests.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index ee4d5d7d72..9296e8efb9 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -176,8 +176,26 @@ std::string EncodeBase64(const std::vector<std::uint8_t>& bytes) { } +namespace { + +void VerifyTessellationInstancePatches() { + ShaderRecompiler::RecompileRequest request; + request.graphics.emplace(); + request.graphics->draw.indexCount = 12u; + request.graphics->draw.instanceCount = 2u; + ShaderRecompiler::TessellationConfiguration configuration{}; + configuration.inputControlPoints = 0u; + request.graphics->tessellation = configuration; + Require(ShaderRecompiler::TessellationInstancePatches(request) == 0u); + request.graphics->tessellation->inputControlPoints = 3u; + Require(ShaderRecompiler::TessellationInstancePatches(request) == 4u); +} + +} + int main() { VerifyMemoryView(); + VerifyTessellationInstancePatches(); const ShaderRecompiler::RequestSerializer serializer; const auto historical = serializer.Deserialize(kHistoricalVersion21); From ed2dec46d982dadea466ce7f7b274348db45319e Mon Sep 17 00:00:00 2001 From: dotaku22 <64598464+dotaku22@users.noreply.github.com> Date: Sat, 24 Oct 2026 06:27:56 +0200 Subject: [PATCH 633/702] fix(agc): tolerate mapping changes during committed snapshots --- .../Execution/src/GuestMemory.cpp | 17 ++- .../tests/execution/CommittedReadMapping.cpp | 103 ++++++++++++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/CommittedReadMapping.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 5e6805a772..7735842922 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -2681,7 +2681,13 @@ namespace { #ifdef _WIN32 bool readPages(std::byte* destination, std::uint64_t source, std::size_t bytes) { SIZE_T copied = 0; - return ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<const void*>(source), destination, bytes, &copied) != 0 && copied == bytes; + if (ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<const void*>(source), destination, bytes, &copied) != 0) { + require(copied == bytes, "short committed guest memory read"); + return true; + } + const auto error = GetLastError(); + require(error == ERROR_PARTIAL_COPY || error == ERROR_NOACCESS || error == ERROR_INVALID_ADDRESS, "cannot read committed guest memory"); + return false; } template <class Visit> @@ -2690,9 +2696,14 @@ void visitReadablePages(std::byte* destination, std::uint64_t source, std::size_ visit(std::size_t{0}, bytes, true); return; } + ForgetPages(static_cast<std::uintptr_t>(source), bytes); for (std::size_t offset = 0; offset < bytes;) { - const auto chunk = std::min<std::size_t>(bytes - offset, PageBytes - static_cast<std::size_t>((source + offset) % PageBytes)); - visit(offset, chunk, readPages(destination + offset, source + offset, chunk)); + const auto cursor = source + offset; + const auto chunk = std::min<std::size_t>(bytes - offset, PageBytes - static_cast<std::size_t>(cursor % PageBytes)); + MEMORY_BASIC_INFORMATION memory{}; + require(VirtualQuery(reinterpret_cast<const void*>(cursor), &memory, sizeof(memory)) == sizeof(memory), "cannot query guest memory after a failed committed read"); + const bool readable = memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0 && readableProtection(memory.Protect & 0xffu); + visit(offset, chunk, readable && readPages(destination + offset, cursor, chunk)); offset += chunk; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CommittedReadMapping.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CommittedReadMapping.cpp new file mode 100644 index 0000000000..78ac2f02b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CommittedReadMapping.cpp @@ -0,0 +1,103 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <atomic> +#include <cstdio> +#include <cstring> +#include <stdexcept> +#include <thread> +#ifdef _WIN32 +#include <windows.h> +#endif + +namespace { +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} +#ifdef _WIN32 +void Run() { + constexpr std::size_t bytes = 65536; + constexpr std::size_t page = 4096; + auto* source = static_cast<std::byte*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, bytes)); + Require(source != nullptr, "cannot reserve snapshot fixture"); + GuestArena::GuestArenaCommit_nid_postfix(source, bytes, PAGE_READWRITE, bytes); + std::memset(source, 0x5a, bytes); + const auto address = reinterpret_cast<std::uintptr_t>(source); + { GuestAllocations::Mutation mutation; mutation.Add(source, bytes, true, true, false); } + std::array<std::byte, bytes> snapshot{}; + const auto prime = [&] { + { GuestAllocations::Mutation mutation; mutation.Protect(source, bytes, true, true, false, [] {}); } + Require(AgcDriver::GuestMemory::Accessible(source, bytes), "snapshot fixture is not readable"); + }; + const auto check = [&](bool hole) { + for (std::size_t index = 0; index < bytes; ++index) { + const auto expected = hole && index >= 2 * page && index < 3 * page ? std::byte{} : std::byte{0x5a}; + Require(snapshot[index] == expected, "committed snapshot lost readable bytes or copied inaccessible bytes"); + } + }; + for (const DWORD protection : {PAGE_NOACCESS, PAGE_READWRITE | PAGE_GUARD}) { + prime(); + DWORD previous = 0; + Require(VirtualProtect(source + 2 * page, page, protection, &previous), "cannot protect snapshot hole"); + snapshot.fill(std::byte{0xff}); + AgcDriver::GuestMemory::ReadCommitted(address, snapshot); + check(true); + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(source + 2 * page, &memory, sizeof(memory)) == sizeof(memory), "cannot query snapshot hole"); + Require(memory.Protect == protection, "snapshot read changed the hole's protection"); + Require(VirtualProtect(source + 2 * page, page, PAGE_READWRITE, &previous), "cannot restore snapshot hole"); + prime(); + AgcDriver::GuestMemory::ReadCommittedUnsynced(address, snapshot); + check(false); + } + prime(); + std::atomic<bool> done{false}; + std::thread mutation([&] { + DWORD previous = 0; + while (!done.load(std::memory_order_acquire)) { + VirtualProtect(source + 2 * page, page, PAGE_NOACCESS, &previous); + VirtualProtect(source + 2 * page, page, PAGE_READWRITE, &previous); + } + }); + bool stable = true; + try { + for (unsigned iteration = 0; iteration < 500; ++iteration) { + AgcDriver::GuestMemory::ReadCommittedUnsynced(address, snapshot); + stable = stable && std::all_of(snapshot.begin(), snapshot.begin() + 2 * page, [](auto value) { return value == std::byte{0x5a}; }); + stable = stable && std::all_of(snapshot.begin() + 3 * page, snapshot.end(), [](auto value) { return value == std::byte{0x5a}; }); + } + } catch (...) { + done.store(true, std::memory_order_release); + mutation.join(); + throw; + } + done.store(true, std::memory_order_release); + mutation.join(); + Require(stable, "concurrent snapshot discarded stable readable pages"); + { GuestAllocations::Mutation mutation; mutation.Remove(source); } + GuestArena::GuestArenaReset_nid_postfix(source, bytes); + GuestArena::GuestArenaRelease_nid_postfix(source, bytes); +} +#endif +} + +int main() { + try { +#ifdef _WIN32 + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(); + device->WaitIdle(); + std::puts("committed snapshot mapping changes passed"); + return 0; +#else + return VulkanTestSkipped; +#endif + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From 464bd6aa2c12a8e8535b5e02d075b5b530b3b063 Mon Sep 17 00:00:00 2001 From: dotaku22 <64598464+dotaku22@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:48:02 +0200 Subject: [PATCH 634/702] test(agc): cover committed copy and comparison recovery on Windows --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 +++ .../tests/execution/CommittedReadMapping.cpp | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6b87a337dd..b069de02e6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2250,6 +2250,9 @@ file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/ set(agcExecutionTests) foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) get_filename_component(agcExecutionTestName ${agcExecutionTestSource} NAME_WE) + if(NOT WIN32 AND agcExecutionTestName STREQUAL "CommittedReadMapping") + continue() + endif() if(ANYPS5_AGC_EXECUTION_TESTS AND NOT agcExecutionTestName IN_LIST ANYPS5_AGC_EXECUTION_TESTS) set(agcExecutionTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/ExecutionTestNotBuilt.cpp) endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CommittedReadMapping.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CommittedReadMapping.cpp index 78ac2f02b4..fb8dcd5ad9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/CommittedReadMapping.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CommittedReadMapping.cpp @@ -48,10 +48,21 @@ void Run() { MEMORY_BASIC_INFORMATION memory{}; Require(VirtualQuery(source + 2 * page, &memory, sizeof(memory)) == sizeof(memory), "cannot query snapshot hole"); Require(memory.Protect == protection, "snapshot read changed the hole's protection"); + Require(!AgcDriver::GuestMemory::Accessible(source, bytes), "failed snapshot retained stale readable page states"); Require(VirtualProtect(source + 2 * page, page, PAGE_READWRITE, &previous), "cannot restore snapshot hole"); prime(); AgcDriver::GuestMemory::ReadCommittedUnsynced(address, snapshot); check(false); + prime(); + Require(VirtualProtect(source + 2 * page, page, protection, &previous), "cannot protect comparison hole"); + std::fill(snapshot.begin() + 2 * page, snapshot.begin() + 3 * page, std::byte{0x77}); + Require(AgcDriver::GuestMemory::EqualsCommitted(address, snapshot), "committed comparison did not skip an inaccessible page"); + snapshot[page - 1] = std::byte{}; + Require(!AgcDriver::GuestMemory::EqualsCommittedUnsynced(address, snapshot), "committed comparison missed a readable byte difference"); + Require(!AgcDriver::GuestMemory::Accessible(source, bytes), "failed comparison retained stale readable page states"); + Require(VirtualQuery(source + 2 * page, &memory, sizeof(memory)) == sizeof(memory), "cannot query comparison hole"); + Require(memory.Protect == protection, "committed comparison changed the hole's protection"); + Require(VirtualProtect(source + 2 * page, page, PAGE_READWRITE, &previous), "cannot restore comparison hole"); } prime(); std::atomic<bool> done{false}; From e5835d6866aa2303924a22eacdd5bd8e72a7ba00 Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:58:39 +0100 Subject: [PATCH 635/702] feat(libc): add C23 count-zeros exports --- core/libs/prx/libc/CMakeLists.txt | 12 ++++ core/libs/prx/libc/include/Api.hpp | 5 ++ core/libs/prx/libc/src/IntegerBuiltins.cpp | 6 ++ core/libs/tests/GuestCountZeros.cpp | 70 ++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 5 files changed, 94 insertions(+) create mode 100644 core/libs/tests/GuestCountZeros.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 6a6373099c..33d78ccd64 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -477,6 +477,18 @@ if(BUILD_TESTING) set_tests_properties(guest_integer_builtins PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test_executable(guest_count_zeros_tests ${LIBS_INCLUDE_DIR}/tests/GuestCountZeros.cpp) + target_include_directories(guest_count_zeros_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_count_zeros_tests PRIVATE libc) + configure_windows_unwind(guest_count_zeros_tests) + foreach(width uc us ui ul ull) + add_test(NAME guest_count_zeros_${width} COMMAND guest_count_zeros_tests ${width}) + set_tests_properties(guest_count_zeros_${width} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_count_zeros_${width} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() + endforeach() add_test_executable(guest_wide_stream_scan_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideStreamScan.cpp) target_include_directories(guest_wide_stream_scan_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_wide_stream_scan_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 81a5cd71c0..e6fe2b1213 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -416,6 +416,11 @@ int APS5_VABI __ctzdi2_nid_postfix(std::uint64_t value); int APS5_VABI __ctzti2_nid_postfix(Uint128 value); int APS5_VABI __ffsdi2_nid_postfix(std::uint64_t value); int APS5_VABI __ffsti2_nid_postfix(Uint128 value); +std::uint32_t APS5_VABI stdc_count_zeros_uc_nid_postfix(std::uint8_t value); +std::uint32_t APS5_VABI stdc_count_zeros_us_nid_postfix(std::uint16_t value); +std::uint32_t APS5_VABI stdc_count_zeros_ui_nid_postfix(std::uint32_t value); +std::uint32_t APS5_VABI stdc_count_zeros_ul_nid_postfix(std::uint64_t value); +std::uint32_t APS5_VABI stdc_count_zeros_ull_nid_postfix(std::uint64_t value); int APS5_VABI __paritysi2_nid_postfix(std::uint32_t value); int APS5_VABI __paritydi2_nid_postfix(std::uint64_t value); int APS5_VABI __parityti2_nid_postfix(Uint128 value); diff --git a/core/libs/prx/libc/src/IntegerBuiltins.cpp b/core/libs/prx/libc/src/IntegerBuiltins.cpp index 44a6e6df13..a8f76c1484 100644 --- a/core/libs/prx/libc/src/IntegerBuiltins.cpp +++ b/core/libs/prx/libc/src/IntegerBuiltins.cpp @@ -212,6 +212,12 @@ int APS5_VABI __ffsti2_nid_postfix(Uint128 value) { return value == 0 ? 0 : CountTrailingZeros128(value) + 1; } +std::uint32_t APS5_VABI stdc_count_zeros_uc_nid_postfix(std::uint8_t value) { return 8 - std::popcount(value); } +std::uint32_t APS5_VABI stdc_count_zeros_us_nid_postfix(std::uint16_t value) { return 16 - std::popcount(value); } +std::uint32_t APS5_VABI stdc_count_zeros_ui_nid_postfix(std::uint32_t value) { return 32 - std::popcount(value); } +std::uint32_t APS5_VABI stdc_count_zeros_ul_nid_postfix(std::uint64_t value) { return 64 - std::popcount(value); } +std::uint32_t APS5_VABI stdc_count_zeros_ull_nid_postfix(std::uint64_t value) { return 64 - std::popcount(value); } + int APS5_VABI __paritysi2_nid_postfix(std::uint32_t value) { return std::popcount(value) & 1; } int APS5_VABI __paritydi2_nid_postfix(std::uint64_t value) { return std::popcount(value) & 1; } diff --git a/core/libs/tests/GuestCountZeros.cpp b/core/libs/tests/GuestCountZeros.cpp new file mode 100644 index 0000000000..8dd64f97aa --- /dev/null +++ b/core/libs/tests/GuestCountZeros.cpp @@ -0,0 +1,70 @@ +#include "prx/libc/include/Api.hpp" +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <limits> + +static void Require(bool condition) { + if (condition) return; + std::fprintf(stderr, "count zeros mismatch\n"); + std::abort(); +} + +template<typename TValue> +static std::uint32_t Reference(TValue value) { + std::uint32_t result = 0; + for (unsigned bit = 0; bit < std::numeric_limits<TValue>::digits; ++bit) { + result += (value & 1) == 0; + value = static_cast<TValue>(value >> 1); + } + return result; +} + +template<typename TValue, typename TCounter> +static void Check(TCounter count) { + constexpr unsigned width = std::numeric_limits<TValue>::digits; + const auto maximum = std::numeric_limits<TValue>::max(); + Require(count(0) == width); + Require(count(maximum) == 0); + if constexpr (width <= 16) { + for (std::uint32_t value = 0; value <= maximum; ++value) { + Require(count(static_cast<TValue>(value)) == Reference(static_cast<TValue>(value))); + } + } + for (unsigned bit = 0; bit < width; ++bit) { + const auto value = static_cast<TValue>(TValue{1} << bit); + Require(count(value) == width - 1); + Require(count(static_cast<TValue>(~value)) == 1); + for (unsigned other = 0; other < width; ++other) { + const auto pair = static_cast<TValue>(value | (TValue{1} << other)); + Require(count(pair) == width - (bit == other ? 1u : 2u)); + } + } + if constexpr (width > 16) { + std::uint64_t state = 0x6b82439d015af7ceULL; + for (unsigned index = 0; index < 100000; ++index) { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + const auto value = static_cast<TValue>(state); + Require(count(value) == Reference(value)); + const auto complement = static_cast<TValue>(~value); + Require(count(complement) == Reference(complement)); + } + } +} + +int main(int argc, char** argv) { + Require(argc == 2); + const int saved = *__error_nid_postfix(); + *__error_nid_postfix() = 87; + if (std::strcmp(argv[1], "uc") == 0) Check<std::uint8_t>(stdc_count_zeros_uc_nid_postfix); + else if (std::strcmp(argv[1], "us") == 0) Check<std::uint16_t>(stdc_count_zeros_us_nid_postfix); + else if (std::strcmp(argv[1], "ui") == 0) Check<std::uint32_t>(stdc_count_zeros_ui_nid_postfix); + else if (std::strcmp(argv[1], "ul") == 0) Check<std::uint64_t>(stdc_count_zeros_ul_nid_postfix); + else if (std::strcmp(argv[1], "ull") == 0) Check<std::uint64_t>(stdc_count_zeros_ull_nid_postfix); + else Require(false); + Require(*__error_nid_postfix() == 87); + *__error_nid_postfix() = saved; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 203ef53b27..14ea1f3e2c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -54,6 +54,7 @@ - [Sized __atomic calls](../../../core/libs/prx/libc/src/DinkumwareAtomics.cpp) (libc) - the 36 `__atomic_load_N`, `__atomic_store_N`, `__atomic_exchange_N`, `__atomic_compare_exchange_N` and `__atomic_fetch_{add,sub,and,or,xor}_N` calls (N = 1, 2, 4, 8) are sequentially consistent whatever order they are given. The `libc.prx` bundled with PPSA08427, run side by side on random values and on every order from -1 to 7, switches on the order but always uses a locked instruction or CAS loop, except that a store with an order other than `memory_order_seq_cst` is a plain store, so these are never weaker than the console. Unlike `_Atomic_load_8` and the 8-byte `_Atomic_*` exchange and fetch calls above, the console accepts every order here, including release, acq_rel and invalid ones, and these do the same - [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. - [feraiseexcept](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc, NID `NIfFNcyeCTo`) - console behavior and module/library versions have not been measured. The signature follows the [public PS5 SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/fenv.h); flag updates and x87 trap delivery follow [FreeBSD amd64 fenv.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/msun/amd64/fenv.c). All six defined exception bits are supported. Unlike the FreeBSD reference, which does not mask or validate the requested bits, this implementation rejects other bits with `std::invalid_argument` before modifying state; console behavior for those bits is unknown. This is distinct from `_Feraise`, which also modifies errno and adds inexact for range exceptions. +- [stdc_count_zeros_uc, us, ui, ul and ull](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) (libc) have not been measured on a console. Their unsigned 8/16/32/64/64-bit arguments and zero-bit counts follow PPSA03671 v01.001.005's bundled libc.prx, read from disassembly and run directly on a Windows host; the ul argument remains 64-bit on Windows. - [_Mtx_timedlock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `hPzYSd5Nasc`) follows the console's `libc.prx` at `0x5d50` (PPSA02530): it takes an absolute xtime and returns `_Thrd_timedout` (2) on expiry (`0x8002003c`) or 5 on other failures. On non-Windows hosts the native mutex and condition are `std::timed_mutex` and `std::condition_variable_any`; Windows keeps the `CRITICAL_SECTION`/`CONDITION_VARIABLE` pair because `std::condition_variable_any` failed `guest_libc_cnd_timed_wait` on the Windows runner, and its `_Mtx_timedlock` polls `TryEnterCriticalSection` until the deadline. Windows timing is not measured on the console. - [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm - [rand, srand and rand_r](../../../core/libs/prx/libc/src/Math.cpp) (libc) follow the `libc.prx` that titles ship (read from disassembly of the copies of PPSA13934, PPSA07923 and PPSA05916, not measured on a console): `rand` steps a 64-bit state, initially 1, as `state * 0x5851F42D4C957F2D + 1` and returns bits 32 to 61, so its results stay at or below `0x3FFFFFFF`; `srand` stores its seed zero-extended as that state; `rand_r` is the minimal standard generator, `state * 16807` modulo 2^31 - 1 on the signed 32-bit state, with a zero state becoming `0x2AE1B767`. The console's `rand` keeps its state without a lock; this one takes `g_randLock`. The console's system libc, which code without a bundled `libc.prx` uses, has a different generator (reported in #9648 from a PS4 Pro); it is not modelled. From 2f25ab919feee34f8663c50369b56071478fa5b7 Mon Sep 17 00:00:00 2001 From: Nicolas Gonzalez <nicolas.gonzalez93@outlook.com> Date: Tue, 27 Oct 2026 23:32:25 -0300 Subject: [PATCH 636/702] fix(shader): fence the LDS lock's critical section on MoltenVK SPIRV-Cross emits the lock's acquire compare-exchange and release exchange as relaxed Metal atomics, so the plain LDS loads and stores of a locked 64-bit update were not ordered against the lock and contended ds_add_u64 lost updates on an Apple M5 Pro. On MoltenVK the driver now sets SpirvTarget::ldsLockFences, and the lock brackets its critical section with workgroup-scope acquire and release OpMemoryBarriers, which become threadgroup atomic_thread_fence with no execution barrier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Execution/include/SubgroupBarrier.hpp | 4 ++ .../Execution/src/VulkanDevice.cpp | 3 ++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../tests/SpirvLdsAtomicLock.cpp | 52 +++++++++++++++++-- .../libSceAgcDriver/tests/SubgroupBarrier.cpp | 7 +++ .../libSceAgcDriver/tools/ShaderReplay.cpp | 8 ++- core/shader/recompiler/CacheKey.hpp | 1 + core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../SpirvMemory/SpirvMemoryInstructions.cpp | 2 + docs/dev/debt/shader.md | 1 + 14 files changed, 79 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp index 8675650a20..d89d7cea43 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp @@ -13,6 +13,10 @@ inline bool WorkgroupScopeFenceUsable(VkDriverId driver) { return driver != VK_DRIVER_ID_AMD_PROPRIETARY; } +inline bool LdsLockFencesRequired(VkDriverId driver) { + return driver == VK_DRIVER_ID_MOLTENVK; +} + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index dd6d622556..483624b1f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -405,6 +405,7 @@ struct VulkanDevice::State { bool exactRobustLoads = false; bool subgroupControlBarriers = true; bool workgroupScopeFences = true; + bool ldsLockFences = false; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; @@ -1305,6 +1306,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->tessellationPipelineCache = TessellationPipelineCacheUsable(driver.driverID); state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); state->workgroupScopeFences = WorkgroupScopeFenceUsable(driver.driverID); + state->ldsLockFences = LdsLockFencesRequired(driver.driverID); } if (driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_NV_SHADER_SM_BUILTINS_EXTENSION_NAME)) { VkPhysicalDeviceShaderSMBuiltinsPropertiesNV smBuiltins{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SM_BUILTINS_PROPERTIES_NV}; @@ -4060,6 +4062,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.largePixelFunctionLdsBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY; target.pixelFunctionStorageDwords = state->pixelFunctionStorageDwords; target.deviceScopeLdsMemoryBarriers = !state->workgroupScopeFences; + target.ldsLockFences = state->ldsLockFences; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index d30a761388..249f639102 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -780,6 +780,7 @@ void verifyKeySensitivity() { changes("exact robust loads", [](SampleRequest& sample) { sample.request.target.exactRobustLoads = true; }); changes("wave LDS memory barriers", [](SampleRequest& sample) { sample.request.target.waveLdsMemoryBarriers = true; }); changes("device-scope LDS memory barriers", [](SampleRequest& sample) { sample.request.target.deviceScopeLdsMemoryBarriers = true; }); + changes("LDS lock fences", [](SampleRequest& sample) { sample.request.target.ldsLockFences = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; }); changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp index 2bb13e5283..5682d5838a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp @@ -24,7 +24,19 @@ constexpr std::uint32_t DsOperand32 = 0x00000401u; constexpr std::uint32_t FlatAtomicAddU32 = 0xdcc90000u; constexpr std::uint32_t FlatAtomicOperands = 0x0a7d0402u; +struct LockFence { + std::uint32_t scope = 0u; + std::uint32_t semantics = 0u; + std::size_t accessesBefore = 0; +}; + +struct LockRegion { + std::vector<LockFence> fences; + std::size_t accesses = 0; +}; + struct Usage { + std::vector<LockRegion> regions; std::size_t elects = 0; std::size_t exchanges = 0; std::size_t releases = 0; @@ -40,7 +52,7 @@ void Require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } -std::vector<std::uint32_t> RecompileProgram(std::span<const std::uint32_t> code) { +std::vector<std::uint32_t> RecompileProgram(std::span<const std::uint32_t> code, bool ldsLockFences) { const std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array<std::uint32_t, 4> userData{}; const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; @@ -59,6 +71,7 @@ std::vector<std::uint32_t> RecompileProgram(std::span<const std::uint32_t> code) request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; request.target.maxWorkgroupInvocations = 1024; request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.ldsLockFences = ldsLockFences; request.layout = {0, 0, 0, 128}; request.useCache = false; auto result = Recompile(request); @@ -101,8 +114,10 @@ Usage Scan(std::span<const std::uint32_t> words) { if (op == spv::OpBranchConditional && count >= 4u && words[cursor + 1u] == laneZeroCondition) laneZeroBody = words[cursor + 2u]; if (op == spv::OpConstant && count == 4) constants[words[cursor + 2]] = words[cursor + 3]; if (op == spv::OpGroupNonUniformElect) ++usage.elects; + if (locked && (op == spv::OpLoad || op == spv::OpStore || op == spv::OpAtomicIAdd || op == spv::OpAtomicCompareExchange)) ++usage.regions.back().accesses; if (op == spv::OpAtomicCompareExchange) { ++usage.exchanges; + if (!locked) usage.regions.emplace_back(); locked = true; if (count >= 4u) lockPointer = words[cursor + 3u]; } @@ -116,7 +131,13 @@ Usage Scan(std::span<const std::uint32_t> words) { ++usage.storeReleases; locked = false; } - if (op == spv::OpMemoryBarrier && locked) usage.fenceBetweenLock = true; + if (op == spv::OpMemoryBarrier && locked) { + usage.fenceBetweenLock = true; + const auto scope = constants.find(words[cursor + 1]); + const auto semantics = constants.find(words[cursor + 2]); + Require(scope != constants.end() && semantics != constants.end(), "a memory barrier inside the workgroup lock has a non-constant scope or semantics"); + usage.regions.back().fences.push_back({scope->second, semantics->second, usage.regions.back().accesses}); + } if (op == spv::OpAtomicIAdd) { ++usage.adds; if (locked) usage.addBetweenLock = true; @@ -156,11 +177,11 @@ Usage Scan(std::span<const std::uint32_t> words) { return usage; } -Usage Compile(std::initializer_list<std::uint32_t> body) { +Usage Compile(std::initializer_list<std::uint32_t> body, bool ldsLockFences = false) { std::vector<std::uint32_t> code{0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u}; code.insert(code.end(), body); code.insert(code.end(), {0xbf8cc07fu, 0xbf810000u}); - return Scan(RecompileProgram(code)); + return Scan(RecompileProgram(code, ldsLockFences)); } void Check64BitOnlyLocks() { @@ -190,6 +211,28 @@ void CheckFlatAtomicTakesLockWith64Bit() { Require(!usage.fenceBetweenLock, "a 32-bit flat atomic into the LDS aperture emits a memory barrier while it holds the workgroup lock"); } +void RequireFencedRegions(const Usage& usage, std::size_t expected, const std::string& name) { + Require(usage.regions.size() == expected, name + " with LDS lock fences took the workgroup lock " + std::to_string(usage.regions.size()) + " times, expected " + std::to_string(expected)); + for (const auto& region : usage.regions) { + Require(region.fences.size() == 2, name + " with LDS lock fences emitted " + std::to_string(region.fences.size()) + " memory barriers inside the workgroup lock, expected 2"); + const auto& acquire = region.fences.front(); + const auto& release = region.fences.back(); + Require(acquire.scope == spv::ScopeWorkgroup && release.scope == spv::ScopeWorkgroup, name + " with LDS lock fences emitted a lock fence that is not workgroup scope"); + Require((acquire.semantics & spv::MemorySemanticsAcquireMask) != 0 && (acquire.semantics & spv::MemorySemanticsWorkgroupMemoryMask) != 0, name + " with LDS lock fences does not start its critical section with an acquire fence on workgroup memory"); + Require((release.semantics & spv::MemorySemanticsReleaseMask) != 0 && (release.semantics & spv::MemorySemanticsWorkgroupMemoryMask) != 0, name + " with LDS lock fences does not end its critical section with a release fence on workgroup memory"); + Require(region.accesses != 0 && acquire.accessesBefore == 0 && release.accessesBefore == region.accesses, name + " with LDS lock fences accesses LDS outside the fences inside the workgroup lock"); + } +} + +void CheckLockFences() { + RequireFencedRegions(Compile({DsAddU64, DsOperands}, true), 1, "ds_add_u64"); + const auto mixed = Compile({DsAddU64, DsOperands, DsAddU32Hi, DsOperand32}, true); + RequireFencedRegions(mixed, 2, "ds_add_u32 with ds_add_u64"); + Require(mixed.adds == 1 && mixed.addBetweenLock, "with LDS lock fences, the 32-bit OpAtomicIAdd is not between the lock acquire and the lock release"); + const auto unfenced = Compile({DsAddU32Hi, DsOperand32}, true); + Require(unfenced.regions.empty() && !unfenced.fenceBetweenLock, "with LDS lock fences, a program with only ds_add_u32 got a workgroup lock"); +} + void CheckFlatAtomicOnlyIsUnchanged() { const auto usage = Compile({FlatAtomicAddU32, FlatAtomicOperands}); Require(usage.elects == 0, "a program with only a 32-bit flat atomic got a workgroup lock: elect " + std::to_string(usage.elects)); @@ -212,6 +255,7 @@ int main() { CheckFlatAtomicTakesLockWith64Bit(); CheckFlatAtomicOnlyIsUnchanged(); Check32BitOnlyIsUnchanged(); + CheckLockFences(); } catch (const std::exception& error) { std::fprintf(stderr, "SpirvLdsAtomicLock: %s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp index 0578ee283d..b3f20c7fa2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp @@ -144,11 +144,18 @@ void verifyEmittedBarriers() { require(single.subgroupControl == 0 && single.subgroupMemory == 0, "with wave LDS memory barriers, a single-wave workgroup did not keep workgroup-scope barriers"); } +void verifyLdsLockFenceDrivers() { + for (const auto& item : Cases) { + require(AgcDriver::LdsLockFencesRequired(item.driver) == (item.driver == VK_DRIVER_ID_MOLTENVK), "driver " + std::to_string(static_cast<int>(item.driver)) + ": expected LDS lock fences only on MoltenVK"); + } +} + } int main() { try { verifyDriverQuirk(); + verifyLdsLockFenceDrivers(); verifyEmittedBarriers(); verifyLdsMemoryBarrierScope(); std::puts("subgroup barrier tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 7bad5361bb..31d7e6b4ae 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -50,6 +50,7 @@ bool g_maintenance8 = false; bool g_robust2 = false; bool g_waveLdsMemoryBarriers = false; bool g_deviceScopeLdsMemoryBarriers = false; +bool g_ldsLockFences = false; bool g_code = false; bool Replay(const char* path) { @@ -151,6 +152,7 @@ bool Replay(const char* path) { if (g_robust2) request.request.target.robustBufferAccess2 = true; if (g_waveLdsMemoryBarriers) request.request.target.waveLdsMemoryBarriers = true; if (g_deviceScopeLdsMemoryBarriers) request.request.target.deviceScopeLdsMemoryBarriers = true; + if (g_ldsLockFences) request.request.target.ldsLockFences = true; const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); if (g_spirv) { @@ -188,7 +190,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] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] [--wave-lds-memory-barriers] [--device-scope-lds-memory-barriers] <shader.req>...\n the driver writes shader_<address>.req files when APS5_DUMP_SHADERS is set; shader_<address>.prepare.req holds a request whose preparation failed before any resource capture, so replay it with --prepare\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] [--wave-lds-memory-barriers] [--device-scope-lds-memory-barriers] [--lds-lock-fences] <shader.req>...\n the driver writes shader_<address>.req files when APS5_DUMP_SHADERS is set; shader_<address>.prepare.req holds a request whose preparation failed before any resource capture, so replay it with --prepare\n"); return 2; } int failures = 0; @@ -221,6 +223,10 @@ int main(int argc, char** argv) { g_deviceScopeLdsMemoryBarriers = true; continue; } + if (std::string(argv[i]) == "--lds-lock-fences") { + g_ldsLockFences = true; + continue; + } if (std::string(argv[i]) == "--mem") { g_memory = true; continue; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 5d8cbad148..d3a89ff348 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -338,6 +338,7 @@ private: append(key, value.largePixelFunctionLdsBuffer); append(key, value.pixelFunctionStorageDwords); append(key, value.deviceScopeLdsMemoryBarriers); + append(key, value.ldsLockFences); } }; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index abc5ff780c..437d284e5e 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -676,6 +676,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) } targetOptions.waveLdsMemoryBarriers = request.target.waveLdsMemoryBarriers; targetOptions.deviceScopeLdsMemoryBarriers = request.target.deviceScopeLdsMemoryBarriers; + targetOptions.ldsLockFences = request.target.ldsLockFences; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 51a0b52768..829f985161 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -331,6 +331,7 @@ struct SpirvTarget { bool largePixelFunctionLdsBuffer = false; std::uint32_t pixelFunctionStorageDwords = 4096u; bool deviceScopeLdsMemoryBarriers = false; + bool ldsLockFences = false; bool fragmentBarycentricBuiltins = false; }; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index ea2212765a..07f0dc29a0 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -43,6 +43,7 @@ struct SpirvTargetOptions { bool largePixelFunctionLdsBuffer = false; std::uint32_t pixelFunctionStorageDwords = 4096u; bool deviceScopeLdsMemoryBarriers = false; + bool ldsLockFences = false; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 4bf8b480aa..b40e89c25b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -131,6 +131,7 @@ struct SpirvEmitterState { std::map<std::pair<std::uint32_t, std::uint32_t>, std::uint32_t> solvedBarycentrics; bool waveLdsMemoryBarriers = false; bool deviceScopeLdsMemoryBarriers = false; + bool ldsLockFences = false; std::unordered_set<const IrValue*> sharedLaneValues; std::uint32_t laneHalf = 0; std::uint32_t spirvVersion = 0x00010300u; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 6e45a0f697..5bf8e747f4 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -282,6 +282,7 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; state.deviceScopeLdsMemoryBarriers = target.deviceScopeLdsMemoryBarriers; + state.ldsLockFences = target.ldsLockFences; state.hostSubgroupSize = target.subgroupSize; if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > target.pixelFunctionStorageDwords) { state.pixelFunctionLdsBuffer = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 78314c2247..cfe05d771d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1394,9 +1394,11 @@ std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std: state.module.AddFunction(spv::OpSelectionMerge, attemptMerge, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, acquired, critical, attemptMerge); EmitLabel(state, critical); + if (state.ldsLockFences) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireMask | LdsMemorySemantics(state))); state.insideLdsLock = true; const auto old = update(); state.insideLdsLock = false; + if (state.ldsLockFences) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsReleaseMask | LdsMemorySemantics(state))); const auto released = state.module.AllocateId(); state.module.AddFunction(spv::OpAtomicExchange, TypeU32(state), released, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsReleaseMask | LdsMemorySemantics(state)), ConstantU32(state, 0u)); const auto criticalExit = state.currentLabel; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 9bd5810262..fa9bc0b67e 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -166,6 +166,7 @@ - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view) are sampled in the shader, the way color comparisons are emulated: `image_sample` of a 2D or 2D array view fetches the one or four texels at LOD 0, decodes them and blends them, for point or bilinear filtering with equal minification and magnification filters and wrap or clamp-to-edge addressing. A shader gets these sample modes only once a converted descriptor reaches one of its sampled images: that capture throws `ConvertedSampleRequired`, the driver records the program and prepares it again with `GuestContext::convertedSamples`, so such a shader is prepared twice per run and its first capture is repeated. The record lives with the registered program and is not kept across runs. Bilinear weights are rounded to the hardware's 8-bit grid (`roundEven(fraction * 256) / 256`, ties to even), as measured with `image_sample_lz` on RDNA2 (gfx1035) for 10_11_11 UNORM texels by adriadam10 in #5958; point sampling, decoding, swizzles and wrap and clamp-to-edge addressing matched the same measurement. A sample that is not `_lz` needs an image with one mip level; gathers, LOD queries, clamp-to-border and mirrored addressing, other filters, min/max reduction, unnormalized coordinates, images selected at run time, 16-bit data and a DST_SEL of the missing W channel throw. - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035; with the workgroup lock, in device memory and in the invocation-private LDS of other stages alike, the recompiler records the `Unaligned` shader fault with the LDS byte address, leaves both dwords unchanged and returns 0, but the lane and the rest of its wave keep running. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. +- [LDS lock ordering on MoltenVK](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - SPIRV-Cross emits the 64-bit LDS lock's acquire compare-exchange and release exchange as `memory_order_relaxed` Metal atomics, which do not order the plain LDS loads and stores inside the lock; contended `ds_add_u64` lost updates on an Apple M5 Pro. On MoltenVK the driver sets `SpirvTarget::ldsLockFences`, and the lock brackets its critical section with a workgroup-scope acquire `OpMemoryBarrier` and a release one, which SPIRV-Cross emits as `atomic_thread_fence(mem_flags::mem_threadgroup, ...)` with no execution barrier. Other drivers do not get these fences, since AMD's Windows driver compiles them to `s_barrier`. Other Apple GPUs were not measured. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) until teardown; excess slots sticky-flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent (prefer device-local). Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. From 97f1d41a2cba33cfe3d26436c3123274705f4d15 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Wed, 28 Oct 2026 15:45:12 +1300 Subject: [PATCH 637/702] Revert "Merge pull request #10809 from frankea/fix/direct-memory-12gib" This reverts commit 780b33aea3, restoring the 13824 MiB direct memory pool. With the pool at 12288 MiB, Astro Bot (PPSA21564) can no longer load its level: DirectMemoryAllocator asserts "Can't allocate direct memory" for a 64 MB VideoPlayerTexture with 12228 of 12288 MB in use, then the process faults about 26 s in (2 of 2 runs, RTX 4090, Linux, main cccdaa2734). The same tree with only this merge reverted runs the full 150 s with no direct memory assert (2 of 2 runs). --- core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp | 2 +- core/libs/tests/GuestAprRead.cpp | 2 +- core/libs/tests/GuestMemory.cpp | 5 ----- docs/dev/debt/libkernel.md | 2 +- docs/user/USAGE.md | 2 +- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index a04a9c3481..604e85b34c 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -7,7 +7,7 @@ #include "prx/libkernel/KernelErrors.hpp" -static constexpr size_t DIRECT_MEMORY_SIZE = 12288ULL * 1024 * 1024; +static constexpr size_t DIRECT_MEMORY_SIZE = 13824ULL * 1024 * 1024; static constexpr size_t PS5_PAGE_SIZE = 0x4000; #ifdef _WIN32 diff --git a/core/libs/tests/GuestAprRead.cpp b/core/libs/tests/GuestAprRead.cpp index 88c16dffc7..da7ee7df7c 100644 --- a/core/libs/tests/GuestAprRead.cpp +++ b/core/libs/tests/GuestAprRead.cpp @@ -68,7 +68,7 @@ private: class DirectBuffer { public: DirectBuffer() { - Require(sceKernelAllocateDirectMemory(0, 12288ll << 20, MappingBytes, 0x4000, 0, &physical) == 0, "cannot allocate direct memory"); + Require(sceKernelAllocateDirectMemory(0, 13824ll << 20, MappingBytes, 0x4000, 0, &physical) == 0, "cannot allocate direct memory"); if (sceKernelMapDirectMemory(&pointer, MappingBytes, 0x33, 0, physical, 0x4000) != 0) { sceKernelReleaseDirectMemory(physical, MappingBytes); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 158985c7e5..be7fc86b3d 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -726,10 +726,6 @@ static void CheckReleaseDirectMemoryClearsMappings() { } } -static void CheckDirectMemorySize() { - Require(sceKernelGetDirectMemorySize() == 0x300000000ull); -} - static void CheckReleaseDirectMemoryRejectsInvalidRanges() { constexpr std::size_t page = 0x4000; Require(sceKernelReleaseDirectMemory(-1, page) == SCE_KERNEL_ERROR_EINVAL); @@ -3588,7 +3584,6 @@ int main(int argc, char** argv) { CheckSavedDirectMemorySurvivesCachePressure(); CheckReusedDirectMemorySections(); CheckReleaseDirectMemoryClearsMappings(); - CheckDirectMemorySize(); CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); CheckFixedMappingOverHoleInsideAllocation(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 61b22528e4..315904e281 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -63,7 +63,7 @@ - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`, as a payload process on PS5 firmware 13.60 got for libkernel's handle, a size of 0, a null info and a bad handle. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Which modules the console lists is in the entry above. PPSA34063 imports both and fails to load without them - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - returns 0 and the main program's first segment, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348) does, where a payload process on PS5 firmware 13.60 got 0 with address 0 and size 0, twice, though libkernel's text was at 0x800000000; what a title gets is not measured. A null address or size faulted there and throws here. libkernel.sprx and libkernel_sys.sprx export the function at the same address - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. The host pages behind a released range stay committed until `sceKernelMunmap`, as `madvise` keeps them for every advice; freeing them would lose the contents the console still returns. -- [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 12 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) +- [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - its arguments follow a payload process on a PS5 (firmware 13.60) ([probes](https://gist.github.com/orezraey/42a06a7ea0d9fb57ac9c06eaf9e6756c)). One answer is not modelled: in a process that had not yet called `sceKernelGetDirectMemorySize` or otherwise touched direct memory, the console returned `EBADF` (`0x80020009`); here the call always searches the pool. Titles open the direct-memory device during start-up, so they are not expected to see it - [sceKernelReleaseDirectMemory](../../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - releases the allocated pages of the range and skips pages that are not allocated, returning 0, while `sceKernelCheckedReleaseDirectMemory` returns `ENOENT` for them, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c206f35a546230d9c9808b823c0f36f5850cd4af/src/core/memory.cpp#L295-L313) does; not measured on a console. Absolum (PPSA28311) releases such a range when it disposes of its video logos - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 0ad3826de4..057f7551c6 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -244,7 +244,7 @@ For bundled title modules, use the `.guest.prx` files the relinker writes into ` On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. With `APS5_MIMALLOC` set, the title loads `mimalloc.dll` and `mimalloc-redirect.dll` from `libs/` and allocates through mimalloc. Copy both from `build/core/libs/libs`; the release archives contain them. Without them the title keeps the C runtime's heap and prints nothing. -On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 12288 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. +On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. A range larger than 64 MiB is imported as one sparse buffer over 64 MiB udmabuf buffers when the device binds sparse memory (RADV, ANV). Without sparse binding, or when the device's sparse block size is larger than 64 KiB, ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. From 664e66e12c858c0e29639be018d66298250342ca Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:05:36 +0000 Subject: [PATCH 638/702] fix(tools): refuse a SELF whose load segments expand the output far past the container unwrap_self copies one output region per PT_LOAD and nothing required the regions to be disjoint in the input. Only the alignment padding was capped, so 200 PT_LOADs that all point at the same 2 MiB of a 2 MiB container produced 400 MiB, and the 65535 program headers the parser accepts could ask for hundreds of gigabytes, ending in MemoryError or an OOM kill instead of a Refused message. The output size is now checked before the buffer is allocated. It may be up to four times the container, and never less than 256 MiB. --- tools/unwrap_self.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index bf07aa2713..47acdbcdf3 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -46,6 +46,8 @@ RECORD_HAS_SEGMENT = 0x800 RECORD_SEGMENT_SHIFT = 20 FIRST_REGION = 0x400 MAX_ALIGNMENT_PADDING = 64 * 1024 * 1024 +MAX_OUTPUT_FACTOR = 4 +MIN_OUTPUT_LIMIT = 256 * 1024 * 1024 PT_LOAD = 1 PT_NOTE = 4 ENTROPY_LIMIT = 7.5 @@ -330,6 +332,10 @@ def unwrap(path, output, verbose=False): if padding > MAX_ALIGNMENT_PADDING: raise Refused("segment alignment requires more than %d bytes of output padding" % MAX_ALIGNMENT_PADDING) cursor = empty_end + output_limit = max(MAX_OUTPUT_FACTOR * len(data), MIN_OUTPUT_LIMIT) + if cursor > output_limit: + raise Refused("the load segments need %d bytes of output, more than the %d allowed for a %d byte container" + % (cursor, output_limit, len(data))) result = bytearray(cursor) for region in regions: From c753d8193c7a489b526ee284438ef258114fa986 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:05:36 +0000 Subject: [PATCH 639/702] test(tools): unwrap a SELF whose load segments share one file range The conversion has to be refused with the output-size message and leave no output file. --- tools/tests/test_unwrap_self.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 9b4dc3d0d1..a64f76f9ab 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -192,6 +192,19 @@ class UnwrapTest(unittest.TestCase): self.assertNotIn("Traceback", result.stderr) self.assertFalse(output.exists()) + def test_many_load_segments_over_one_range_are_refused_instead_of_exhausting_memory(self): + segments = [(PT_LOAD, 5, 0x100000, 0x400000 + index * 0x400000, 0x200000, 1) for index in range(200)] + container = build_container(segments, [], {0x100000: bytes(0x200000)}) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + source.write_bytes(container) + result = subprocess.run([sys.executable, str(Path(unwrap_self.__file__)), str(source), str(output)], + capture_output=True, text=True, timeout=60) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("bytes of output", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse(output.exists()) + def test_write_failure_preserves_output_and_removes_temporary_files(self): directory = Path(self.directory) source = directory / "input.self" From 5ceaa3c2769911d6103172b8e48b92e330f06a3e Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:35:38 +0530 Subject: [PATCH 640/702] test(tools): cover report alias rejection in progress_history --- tools/tests/test_progress_history.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index b9be9c7b25..4f4e8f296c 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -207,6 +207,28 @@ class ProgressHistoryTests(unittest.TestCase): data = json.loads((output / "history.json").read_text(encoding="utf-8")) self.assertEqual(data, [point]) + def test_main_rejects_aliased_output_reports(self): + point = {"date": "2026-01-01", "commit": "abc1234", + "libraries": {"done": 1, "total": 2, "percent": 50.0}, + "shaders": {"done": 3, "total": 4, "percent": 75.0}} + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "site" + output.mkdir(parents=True) + report = output / "history.json" + alias = output / "history.html" + report.write_text("existing json", encoding="utf-8") + try: + os.link(report, alias) + except OSError: + self.skipTest("hard links unavailable") + errors = io.StringIO() + with patch.object(progress_history, "collect", return_value=[point]): + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + progress_history.main([str(output)]) + self.assertEqual(error.exception.code, 2) + self.assertEqual(report.read_text(encoding="utf-8"), "existing json") + self.assertEqual(alias.read_text(encoding="utf-8"), "existing json") + if __name__ == "__main__": From f3a612cd5e962ffbf6273905fe87c33e9e81222e Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:35:43 +0530 Subject: [PATCH 641/702] fix(tools): reject aliased report output paths in progress_history --- tools/progress_history.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/progress_history.py b/tools/progress_history.py index 88e253be79..6a74f0f41b 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -232,9 +232,13 @@ def main(argv=None): parser.add_argument("--ref", default="HEAD", help="branch or commit whose first-parent history is measured") parser.add_argument("--cache", type=Path, help="reuse per-commit counts while the scanner, ISA list and Python version match") args = parser.parse_args(argv) + outputs = [args.output / name for name in ("history.json", "history.svg", "history.html")] try: + for index, path in enumerate(outputs): + for other in outputs[index + 1:]: + if (path.exists() and other.exists() and path.samefile(other)) or path.resolve() == other.resolve(): + raise ValueError(f"generated reports cannot alias each other: {path.name} and {other.name}") if args.cache: - outputs = [args.output / name for name in ("history.json", "history.svg", "history.html")] if any(args.cache.resolve() == path.resolve() or (args.cache.exists() and path.exists() and args.cache.samefile(path)) for path in outputs): raise ValueError("history cache must differ from generated reports") From 72eb12c5c1435a061409727c2508702a38e7ee9a Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:06:58 +0000 Subject: [PATCH 642/702] fix(tools): report unreadable --body and --event inputs in check_conventions as usage errors --- tools/check_conventions.py | 18 ++++++++- .../tests/test_check_conventions_body_bom.py | 38 ++++++++++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index b0356e4b19..b564fba26b 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -768,6 +768,20 @@ if __name__ == "__main__": parser.add_argument("--body", help="file with the pull request description to check") parser.add_argument("--event", default=os.environ.get("GITHUB_EVENT_PATH"), help="GitHub event JSON (set in CI)") args = parser.parse_args() + event = {} + if args.event: + try: + event = json.loads(Path(args.event).read_text(encoding="utf-8")) + except (OSError, ValueError) as error: + parser.error(f"--event {args.event}: {error}") + if not isinstance(event, dict): + parser.error(f"--event {args.event}: not a JSON object") + body = "" + if args.body: + try: + body = Path(args.body).read_text(encoding="utf-8-sig") + except (OSError, ValueError) as error: + parser.error(f"--body {args.body}: {error}") check = Check(args.base, args.head) check.code() check.build() @@ -778,9 +792,9 @@ if __name__ == "__main__": check.workflows() check.commits() check.env_reference() - pr = json.load(open(args.event, encoding="utf-8")).get("pull_request") if args.event else None + pr = event.get("pull_request") if pr: check.pull_request(pr["title"], pr["body"]) elif args.title is not None or args.body: - check.pull_request(args.title or "", open(args.body, encoding="utf-8-sig").read() if args.body else "") + check.pull_request(args.title or "", body) raise SystemExit(output(check, os.environ.get("GITHUB_REPOSITORY", "boykopovar/AnyPS5"))) diff --git a/tools/tests/test_check_conventions_body_bom.py b/tools/tests/test_check_conventions_body_bom.py index 2bc30ad43f..527c43fa5a 100644 --- a/tools/tests/test_check_conventions_body_bom.py +++ b/tools/tests/test_check_conventions_body_bom.py @@ -1,3 +1,4 @@ +import json import os import subprocess import sys @@ -12,7 +13,7 @@ BODY = ( class BodyBomTests(unittest.TestCase): - def run_checker(self, body): + def run_checker(self, body, arguments=None, event=None): checker = Path(__file__).resolve().parents[1] / "check_conventions.py" with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -29,9 +30,11 @@ class BodyBomTests(unittest.TestCase): git("add", ".") git("commit", "-qm", "docs: add readme") (root / "body.md").write_bytes(body) + if event is not None: + (root / "event.json").write_bytes(event) return subprocess.run( [sys.executable, "-I", str(checker), "--base", "HEAD", "--head", "HEAD", - "--title", "fix: x", "--body", "body.md"], + *(arguments or ["--title", "fix: x", "--body", "body.md"])], cwd=root, capture_output=True, text=True, encoding="utf-8", env={key: value for key, value in os.environ.items() if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")}, @@ -46,6 +49,37 @@ class BodyBomTests(unittest.TestCase): result = self.run_checker(BODY.encode("utf-8")) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def assertUsageError(self, result, option): + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn(f"error: {option} ", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + + def test_missing_body_is_a_usage_error(self): + result = self.run_checker(BODY.encode("utf-8"), ["--title", "fix: x", "--body", "absent.md"]) + self.assertUsageError(result, "--body") + + def test_body_that_is_not_utf8_is_a_usage_error(self): + result = self.run_checker(b"\xff\xfe" + BODY.encode("utf-8")) + self.assertUsageError(result, "--body") + + def test_missing_event_is_a_usage_error(self): + result = self.run_checker(BODY.encode("utf-8"), ["--event", "absent.json"]) + self.assertUsageError(result, "--event") + + def test_malformed_event_is_a_usage_error(self): + result = self.run_checker(BODY.encode("utf-8"), ["--event", "event.json"], b"{") + self.assertUsageError(result, "--event") + + def test_event_that_is_not_an_object_is_a_usage_error(self): + result = self.run_checker(BODY.encode("utf-8"), ["--event", "event.json"], b"[]") + self.assertUsageError(result, "--event") + + def test_event_pull_request_body_is_checked(self): + event = json.dumps({"pull_request": {"title": "fix: x", "body": BODY}}).encode("utf-8") + result = self.run_checker(b"", ["--event", "event.json"], event) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + if __name__ == "__main__": unittest.main() From aa64282ba19e1a4bb18848ac3fa9ef243ce65bd3 Mon Sep 17 00:00:00 2001 From: Felipebmaitan <felipebmaitan@protonmail.com> Date: Wed, 28 Oct 2026 00:19:30 -0300 Subject: [PATCH 643/702] fix(libSceFreeType): declare advance queries in public API --- core/libs/prx/libSceFreeType/include/Api.hpp | 2 ++ core/libs/prx/libSceFreeType/src/Advances.cpp | 1 + core/libs/tests/GuestFreeTypeAdvances.cpp | 14 ++------------ 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 1135668bad..38dd6befc3 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -27,6 +27,8 @@ int APS5_VABI FT_Get_PFR_Advance_nid_postfix(FreeType::GuestFaceRec* face, std:: int APS5_VABI FT_Get_PFR_Kerning_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t left, std::uint32_t right, FreeType::GuestVector* vector); int APS5_VABI FT_Get_PFR_Metrics_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t* outline, std::uint32_t* metrics, std::int64_t* xScale, std::int64_t* yScale); int APS5_VABI FT_Load_Glyph_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t glyphIndex, std::int32_t loadFlags); +int APS5_VABI FT_Get_Advance_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t glyphIndex, std::int32_t flags, std::int64_t* advance); +int APS5_VABI FT_Get_Advances_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t start, std::uint32_t count, std::int32_t flags, std::int64_t* advances); int APS5_VABI FT_Get_Gasp_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t ppem); int APS5_VABI FT_Get_CID_Registry_Ordering_Supplement_nid_postfix(FreeType::GuestFaceRec* face, const char** registry, const char** ordering, std::int32_t* supplement); int APS5_VABI FT_Get_CID_Is_Internally_CID_Keyed_nid_postfix(FreeType::GuestFaceRec* face, std::uint8_t* isCid); diff --git a/core/libs/prx/libSceFreeType/src/Advances.cpp b/core/libs/prx/libSceFreeType/src/Advances.cpp index 17e410c65d..a4e1cae36c 100644 --- a/core/libs/prx/libSceFreeType/src/Advances.cpp +++ b/core/libs/prx/libSceFreeType/src/Advances.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceFreeType/libSceFreeType.hpp" #include "prx/libSceFreeType/include/GuestObjects.hpp" #include "prx/libc/include/General.hpp" diff --git a/core/libs/tests/GuestFreeTypeAdvances.cpp b/core/libs/tests/GuestFreeTypeAdvances.cpp index da91613b2e..8aeb14f403 100644 --- a/core/libs/tests/GuestFreeTypeAdvances.cpp +++ b/core/libs/tests/GuestFreeTypeAdvances.cpp @@ -1,5 +1,5 @@ #include "FreeTypeFontFixture.hpp" -#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libSceFreeType/libSceFreeType.hpp" #include <ft2build.h> #include FT_FREETYPE_H @@ -17,16 +17,6 @@ using namespace FreeType; -extern "C" { -int APS5_VABI FT_Init_FreeType_nid_postfix(void**); -int APS5_VABI FT_Done_FreeType_nid_postfix(void*); -int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); -int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t); -int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); -int APS5_VABI FT_Get_Advance_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t, std::int64_t*); -int APS5_VABI FT_Get_Advances_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t, std::int32_t, std::int64_t*); -} - static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "FreeType advance check failed at line %d\n", line); @@ -41,7 +31,7 @@ struct Fixture { std::vector<unsigned char> bytes; FT_Library library = nullptr; FT_Face native = nullptr; - void* guestLibrary = nullptr; + GuestLibrary* guestLibrary = nullptr; GuestFaceRec* guest = nullptr; explicit Fixture(std::vector<unsigned char> font) : bytes(std::move(font)) { From 9c8405acc5ab08ae8d3af9c1ea8ffcfc3191a037 Mon Sep 17 00:00:00 2001 From: hyvvs <hyvvs@proton.me> Date: Wed, 28 Oct 2026 04:21:59 +0100 Subject: [PATCH 644/702] fix(tools): classify separate environment reads independently --- tools/env_reference.py | 34 ++++++++++++++++++++++++++++++- tools/tests/test_env_reference.py | 33 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/tools/env_reference.py b/tools/env_reference.py index 7d250577a7..b9a0a8775f 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -79,6 +79,37 @@ def default_of(match, window): return found.group(1) if found else "" +def expression_window(text, code, matches, match, start, end): + peers = sorted((other.start() for _, other in matches if start <= other.start() < end)) + if len(peers) < 2: + return text[start:end] + peer_set = set(peers) + depths = {} + commas = [] + depth = 0 + for pos in range(start, end): + if pos in peer_set: + depths[pos] = depth + token = code[pos] + if token in "([": + depth += 1 + elif token in ")]": + depth -= 1 + elif token == ",": + commas.append((pos, depth)) + for pos, comma_depth in commas: + left = bisect.bisect_left(peers, pos) + if not left or left == len(peers): + continue + if comma_depth > min(depths[peers[left - 1]], depths[peers[left]]): + continue + if pos < match.start(): + start = max(start, pos + 1) + elif pos >= match.end(): + end = min(end, pos) + return text[start:end] + + def source_files(root): root = Path(root) if not root.is_dir(): @@ -106,7 +137,8 @@ def reads(roots, prefixes=PREFIXES): number = bisect.bisect_right(starts, match.start()) start = max(code.rfind(token, 0, match.start()) for token in ";{}") + 1 end = code.find(";", match.end()) - window = text[start:end + 1] if end >= 0 else text[start:] + end = end + 1 if end >= 0 else len(text) + window = expression_window(text, code, matches, match, start, end) found.setdefault(match.group("name"), []).append( (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) for sites in found.values(): diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index 8a1de3df87..f2cfc9885f 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -142,6 +142,39 @@ class EnvReferenceTests(unittest.TestCase): found = env_reference.reads([root / "core"]) self.assertEqual(found["APS5_VALUE"], [(source.as_posix(), 1, kind, default)]) + def test_multiple_reads_in_one_statement_keep_independent_kinds_and_defaults(self): + cases = ( + ('consume(std::getenv("APS5_STRING"),\n\n\n\n' + 'std::getenv("APS5_PRESENT") != nullptr);', + {"APS5_STRING": ("string", ""), "APS5_PRESENT": ("presence", "")}), + ('consume(std::getenv("APS5_FIRST_PRESENT") != nullptr,\n\n\n\n' + 'std::getenv("APS5_LAST_STRING"));', + {"APS5_FIRST_PRESENT": ("presence", ""), "APS5_LAST_STRING": ("string", "")}), + ('consume(atoi(std::getenv("APS5_COUNT")),\n\n\n\n' + 'std::getenv("APS5_OTHER_STRING"));', + {"APS5_COUNT": ("integer", ""), "APS5_OTHER_STRING": ("string", "")}), + ('consume(std::getenv("APS5_FALLBACK") ? 1 : 42,\n\n\n\n' + 'std::getenv("APS5_NO_DEFAULT"));', + {"APS5_FALLBACK": ("string", "42"), "APS5_NO_DEFAULT": ("string", "")}), + ('consume(atoi((std::puts(R"tag(;,{})tag"), std::getenv("APS5_NESTED_COUNT"))),\n' + 'std::getenv("APS5_NEXT_STRING"));', + {"APS5_NESTED_COUNT": ("integer", ""), "APS5_NEXT_STRING": ("string", "")}), + ) + root = self.fixture() + source = root / "core" / "Sample.cpp" + for text, expected in cases: + with self.subTest(source=text): + source.write_text(text + "\n", encoding="utf-8") + found = env_reference.reads([root / "core"]) + self.assertEqual(set(found), set(expected)) + for name, classification in expected.items(): + self.assertEqual(found[name][0][2:], classification) + with contextlib.redirect_stdout(io.StringIO()) as output: + self.assertEqual(env_reference.main(["--root", str(root / "core"), "--table"]), 0) + for name, (kind, default) in expected.items(): + self.assertIn(f"| `{name}` | {kind} |", output.getvalue()) + self.assertIn(f"| {default} |", output.getvalue()) + def test_whitespace_before_parentheses_preserves_names_kinds_and_defaults(self): root = self.fixture() source = root / "core" / "Sample.cpp" From c6e74df78d62c24584305345b8b67cfe08447d0c Mon Sep 17 00:00:00 2001 From: XairRrR <dev910382@gmail.com> Date: Wed, 21 Oct 2026 15:38:11 +0200 Subject: [PATCH 645/702] fix(agc): run queue 0 work after earlier wait-free async submissions Wait-free submissions on other queues ran on their own workers with no order against queue 0, so a queue 0 submission received after them could execute first. Uncharted: The Lost Legacy submits small compute jobs on queue 0x50 and reuses their command and descriptor memory once later queue 0 work has passed; with the async job up to a second late, it read recycled memory (garbage descriptors, BDA faults, a zeroed PM4 buffer) while loading Sri Lanka. A queue 0 submission now waits until no wait-free submission received before it is pending or executing on another queue. Submissions that wait for something, and the wait-free ones queued behind them, are not waited for, since they can wait for this queue 0 work. Uncharted 4's menu fps is unchanged (10.21 vs 10.24, two runs each). Tests: testQueue0SubmissionAfterEarlierWaitFreeWork and testQueue0SubmissionBehindWaitingQueue. AI-assisted: yes --- .../Execution/include/Driver/Driver.hpp | 1 + .../include/Driver/Queues/Submission.hpp | 2 ++ .../src/Driver/Queues/QueueWorker.cpp | 32 +++++++++++++++---- .../src/Driver/Queues/Submission.cpp | 12 +++++++ .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 27 ++++++++++++++++ 5 files changed, 67 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 7d89a2589f..19f2ad9ed1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -143,6 +143,7 @@ private: void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites, const std::shared_ptr<SubmissionProgress>& progress); static bool waitFree(const Submission& submission); bool queue0Before(std::uint64_t received) const; + bool waitFreeBefore(std::uint64_t received) const; bool orderReleased(std::uint32_t queue, std::uint64_t received) const; void noteWaitBlocked(std::uint32_t queue, std::uint64_t awaited, bool blocked); void reportPresents(double waitedMs, std::size_t inFlight); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp index e45f794362..fdd4ff1baa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp @@ -53,6 +53,8 @@ struct QueueWorker { std::atomic<std::uint64_t> queued{0}; std::unordered_map<std::uint64_t, std::uint32_t> unfinishedWrites; + std::uint64_t executingWaitFree = 0; + bool executingWaits = false; std::thread thread; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index b6a5b5689c..34ca75b543 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -243,15 +243,29 @@ void Driver::run(std::uint32_t id) noexcept { pending.pop_front(); if (APS5_ENABLE_TIMING_LOG) submission.dequeuedAt = std::chrono::steady_clock::now(); if (id == 0) queue0Executing = submission.suspend ? 0 : submission.received; + struct OrderHold { + std::atomic<std::uint32_t>& holders; + explicit OrderHold(std::atomic<std::uint32_t>& holders) : holders(holders) { holders.fetch_add(1, std::memory_order_acq_rel); } + ~OrderHold() { holders.fetch_sub(1, std::memory_order_acq_rel); } + OrderHold(const OrderHold&) = delete; + OrderHold& operator=(const OrderHold&) = delete; + }; + if (id == 0 && !submission.suspend) { + { + OrderHold hold{orderHolders}; + const auto released = [&] { return failure || stopping || !waitFreeBefore(submission.received); }; + StallWatch stall(id); + while (!released() && !poll(released)) stall.Check("order behind other queues for event", submission.received); + } + rethrowFailure(); + } + if (id != 0) { + worker.executingWaitFree = submission.waitFree ? submission.received : 0; + worker.executingWaits = !submission.waitFree && !submission.suspend; + } if (submission.waitFree) { { - struct OrderHold { - std::atomic<std::uint32_t>& holders; - explicit OrderHold(std::atomic<std::uint32_t>& holders) : holders(holders) { holders.fetch_add(1, std::memory_order_acq_rel); } - ~OrderHold() { holders.fetch_sub(1, std::memory_order_acq_rel); } - OrderHold(const OrderHold&) = delete; - OrderHold& operator=(const OrderHold&) = delete; - } hold{orderHolders}; + OrderHold hold{orderHolders}; const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); }; StallWatch stall(id); while (!released() && !poll(released)) stall.Check("order behind queue 0 for event", submission.received); @@ -279,6 +293,10 @@ void Driver::run(std::uint32_t id) noexcept { forgetUnfinishedWrites(workers.at(id), submission); } if (id == 0) queue0Executing = 0; + if (id != 0) { + workers.at(id).executingWaitFree = 0; + workers.at(id).executingWaits = false; + } notify = idleWaiters != 0 || orderHolders.load(std::memory_order_acquire) != 0; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index b1b48ac743..7b452b32f3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -539,6 +539,18 @@ bool Driver::queue0Before(std::uint64_t received) const { return false; } +bool Driver::waitFreeBefore(std::uint64_t received) const { + for (const auto& [queue, worker] : workers) { + if (queue == 0 || worker.executingWaits) continue; + if (worker.executingWaitFree != 0 && worker.executingWaitFree < received) return true; + for (const auto& pending : worker.pending) { + if (!pending.waitFree || pending.received >= received) break; + return true; + } + } + return false; +} + bool Driver::orderReleased(std::uint32_t queue, std::uint64_t received) const { if (!queue0Before(received)) return true; const auto awaited = queue0Awaited.load(std::memory_order_acquire); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index e01a2fe1b3..e4f1bc07cd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -443,6 +443,31 @@ void testWaitFreeSubmissionBehindHeldOne() { AgcDriverWaitIdle_nid_postfix(); } +void testQueue0SubmissionAfterEarlierWaitFreeWork() { + alignas(64) static volatile std::uint32_t first = 0, second = 0; + constexpr std::uint32_t writes = 20000; + std::vector<std::uint32_t> words; + for (std::uint32_t i = 1; i <= writes; ++i) { + const auto write = writeData(&first, i); + words.insert(words.end(), write.begin(), write.end()); + } + submit(0x20, words); + submit(0, commands(writeData(&second, 1))); + waitFor(&second, 1, "queue 0's submission never ran"); + check(first == writes, "queue 0 ran a submission before another queue's earlier wait-free submission finished"); + AgcDriverWaitIdle_nid_postfix(); +} + +void testQueue0SubmissionBehindWaitingQueue() { + alignas(64) static volatile std::uint32_t label = 0, other = 0, done = 0; + submit(0x20, commands(waitEqual(&label, 1))); + submit(0x20, commands(writeData(&other, 1))); + submit(0, commands(writeData(&label, 1), writeData(&done, 1))); + check(waitFor(&done, 1, "queue 0 never ran while another queue waited on it") < std::chrono::milliseconds(500), "queue 0 waited for a wait-free submission queued behind another queue's wait on queue 0"); + waitFor(&other, 1, "the wait-free submission behind the satisfied wait never ran"); + AgcDriverWaitIdle_nid_postfix(); +} + void testMultiAcbs() { alignas(64) static volatile std::uint32_t value = 0, done = 0, gate = 0; check(sceAgcDriverSubmitMultiAcbs(0x20, nullptr, nullptr, 0) == 0, "empty multi-ACB submit failed"); @@ -1040,6 +1065,8 @@ int main(int argc, char** argv) { testMultiAcbs(); testWaitFreeSubmissionBehindHeldOne(); testWaitFreeSubmissionTheCpuWaitsFor(); + testQueue0SubmissionAfterEarlierWaitFreeWork(); + testQueue0SubmissionBehindWaitingQueue(); testMultiSubmissions(); testShaderHeaderAlignment(); testHeaderWithoutProgramAddress(); From 86f97e8ac43df95457e8866f11a0d89796cd08e4 Mon Sep 17 00:00:00 2001 From: XairRrR <dev910382@gmail.com> Date: Sat, 17 Oct 2026 02:07:47 +0200 Subject: [PATCH 646/702] fix(recompiler): take a paired wave64 zero branch when some invocations are inactive A wave64 program held as 32 invocations x 2 lanes decided s_cbranch_execz, s_cbranch_vccz and s_cbranch_scc0 by balloting the per-lane "zero" condition and requiring all 64 bits set. OpGroupNonUniformBallot sets the bit of an inactive invocation to 0 (SPIR-V specification, OpGroupNonUniformBallot: "The bit is set to one if the corresponding invocation is active and the Predicate for that invocation evaluated to true; otherwise, it is set to zero"), and an invocation that returned from the entry point is no longer active. When one invocation stopped, e.g. after a BDA fault, the zero branch was never taken for the others: a loop leaving through s_cbranch_execz never ended. The RDNA2 ISA branches when the whole register is zero ("if (EXEC == 0)", "if (VCC == 0)", "if (SCC == 0)", SOPP S_CBRANCH_EXECZ / VCCZ / SCC0), so the branch now asks whether any active invocation still has a lane with the bit set, as the wave32 path does: not any(own lane set || other lane set). Inactive invocations then count as lanes with the bit clear. The non-zero kinds keep any(own || other). The execution test runs a wave64 program in which lane 0 faults and stops its invocation, then takes each zero and non-zero branch; before this change the other lanes fell through all three zero branches. AI-assisted: yes --- .../execution/PairedWaveZeroBranches.cpp | 151 ++++++++++++++++++ .../SpirvBackend/src/SpirvFlowEmitter.cpp | 18 +-- 2 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PairedWaveZeroBranches.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PairedWaveZeroBranches.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PairedWaveZeroBranches.cpp new file mode 100644 index 0000000000..15d3b89297 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PairedWaveZeroBranches.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#else +#include <sys/mman.h> +#endif +#include <array> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <span> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t OutputOffset = 4096; +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::uint32_t Never = 0xffffffffu; + +alignas(256) constexpr std::array<std::uint32_t, 37> BranchesCode{ + 0x7d840080u, 0x7e040200u, 0x7e060201u, 0xd5010002u, 0x01a80502u, 0xd5010003u, 0x01a80703u, 0xdc308000u, + 0x017d0002u, 0xbf8c3f70u, 0xbe880380u, 0xbe8a047eu, 0x7da40006u, 0xbf880001u, 0x88088108u, 0xbefe040au, + 0x7d840006u, 0xbf860001u, 0x88088208u, 0xbf068006u, 0xbf840001u, 0x88088408u, 0x7daa0006u, 0xbf890001u, + 0x88088808u, 0xbefe040au, 0x7d8a0006u, 0xbf870001u, 0x88089008u, 0xbf078006u, 0xbf850001u, 0x8808a008u, + 0x34080082u, 0x4a0a0208u, 0xdc708000u, 0x00040504u, 0xbf810000u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "paired wave zero branches: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +class Reserved { +public: + Reserved() { +#ifdef _WIN32 + range = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE, PAGE_NOACCESS); +#else + range = mmap(nullptr, BlockBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (range == MAP_FAILED) range = nullptr; +#endif + Require(range != nullptr, "paired wave zero branches: cannot reserve an inaccessible range"); + } + ~Reserved() { +#ifdef _WIN32 + VirtualFree(range, 0, MEM_RELEASE); +#else + munmap(range, BlockBytes); +#endif + } + Reserved(const Reserved&) = delete; + Reserved& operator=(const Reserved&) = delete; + std::uint64_t Address() const { return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(range)); } + +private: + void* range = nullptr; +}; + +std::array<std::uint32_t, Lanes> Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, const Reserved& unmapped) { + std::memset(guest.Data(), 0, BlockBytes); + auto* output = guest.Data() + OutputOffset; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) std::memcpy(output + lane * 4u, &Sentinel, 4u); + const auto data = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(guest.Data())); + const auto out = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output)); + const std::vector<std::uint32_t> userData{ + static_cast<std::uint32_t>(data), static_cast<std::uint32_t>(data >> 32u), + static_cast<std::uint32_t>(unmapped.Address()), static_cast<std::uint32_t>(unmapped.Address() >> 32u), + static_cast<std::uint32_t>(out), static_cast<std::uint32_t>(out >> 32u), + Never, + }; + const std::span<const std::uint32_t> code(BranchesCode); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}}, + {64, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.target.spannedWaves = false; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device.WaitIdle(); + std::array<std::uint32_t, Lanes> words{}; + std::memcpy(words.data(), output, sizeof(words)); + return words; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + Reserved unmapped; + const auto words = Run(*device, guest, unmapped); + Require(words[0] == Sentinel, "paired wave zero branches: lane 0 kept running after its faulting read (wrote " + std::to_string(words[0]) + ")"); + const std::array<const char*, 6> branches{"s_cbranch_execz", "s_cbranch_vccz", "s_cbranch_scc0", "s_cbranch_execnz", "s_cbranch_vccnz", "s_cbranch_scc1"}; + for (std::uint32_t lane = 1; lane < Lanes; ++lane) { + if (words[lane] == Sentinel && lane == Lanes / 2u) continue; + for (std::uint32_t bit = 0; bit < branches.size(); ++bit) { + Require((words[lane] & (1u << bit)) == 0u, std::string("paired wave zero branches: lane ") + std::to_string(lane) + " fell through " + branches[bit] + " after another invocation of its wave stopped (wrote " + std::to_string(words[lane]) + ")"); + } + Require(words[lane] == 0u, "paired wave zero branches: lane " + std::to_string(lane) + " wrote " + std::to_string(words[lane]) + ", expected 0"); + } + std::puts("paired wave zero branch tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 13322cf11a..a48917de1f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -180,17 +180,13 @@ std::uint32_t EmitBranchCondition(SpirvValueEmitContext& ctx, const BlockInfo& i return ctx.Def(info.condition); } auto& state = ctx.state; - const auto ballot = ctx.Ballot(info.condition); - const auto low = state.module.AllocateId(); - const auto high = state.module.AllocateId(); - const auto result = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, ballot, 0u); - state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, ballot, 1u); - const auto kind = info.terminator.condition; - const bool zero = kind == BranchCondition::ExecZero || kind == BranchCondition::VccZero || kind == BranchCondition::SccZero; - const auto combined = EmitBinaryU32(state, zero ? spv::OpBitwiseAnd : spv::OpBitwiseOr, low, high); - state.module.AddFunction(zero ? spv::OpIEqual : spv::OpINotEqual, TypeBool(state), result, combined, ConstantU32(state, zero ? ~0u : 0u)); - return result; + const auto own = ctx.Def(info.condition); + const auto other = ctx.otherHalf->Def(info.condition); + if (condition == BranchCondition::ExecZero || condition == BranchCondition::VccZero || condition == BranchCondition::SccZero) { + const auto clear = Binary(state, spv::OpLogicalOr, TypeBool(state), Unary(state, spv::OpLogicalNot, TypeBool(state), own), Unary(state, spv::OpLogicalNot, TypeBool(state), other)); + return Unary(state, spv::OpLogicalNot, TypeBool(state), EmitWaveAny(state, clear)); + } + return EmitWaveAny(state, Binary(state, spv::OpLogicalOr, TypeBool(state), own, other)); } void EmitStructuredTerminator(SpirvValueEmitContext& ctx, const IrProgram& program, const BlockInfo& info) { From 7fae9a16565a4135b30ea59f64e73dcca34c56a9 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:24:43 +0000 Subject: [PATCH 647/702] fix(libSceNpCommon): count the text already in the buffer in sceNpUtilJsonEscape sceNpUtilJsonEscape appends to the destination with strcat, but the room check started its running length at 0 and ignored the text the buffer already held, so escaping into a partly filled buffer wrote past its end: sceNpUtilJsonEscape(buf, 16, "\n\n\n", 3) on a buffer holding ten characters placed 16 characters and the terminator in 16 bytes. The running length now starts at the length of the existing text, and a destination that is already full returns NP_UTIL_ERROR_BUFFER_TOO_SMALL. --- core/libs/prx/libSceNpCommon/Strings.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpCommon/Strings.cpp b/core/libs/prx/libSceNpCommon/Strings.cpp index 08125a4c0f..8a2ec4ba0c 100644 --- a/core/libs/prx/libSceNpCommon/Strings.cpp +++ b/core/libs/prx/libSceNpCommon/Strings.cpp @@ -437,7 +437,8 @@ int APS5_VABI sceNpStrncpyToBuf(char* dst, std::size_t size, const char* src, st int APS5_VABI sceNpUtilJsonEscape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength) { if (dst == nullptr || dstSize == 0 || src == nullptr || srcLength == 0) return NP_UTIL_ERROR_INVALID_ARGUMENT; if (dstSize < JSON_ESCAPE_MARGIN) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; - std::size_t written = 0; + std::size_t written = ::strnlen(dst, dstSize); + if (written >= dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; for (std::size_t i = 0; i < srcLength; ++i) { if (written > dstSize - JSON_ESCAPE_MARGIN) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; const auto c = static_cast<unsigned char>(src[i]); From 79245feab91a1cbfd6cfd84688133942d7048dc7 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:24:44 +0000 Subject: [PATCH 648/702] fix(libSceNpCommon): require the second \u of a surrogate pair in sceNpUtilJsonGetOneChar After a high surrogate, sceNpUtilJsonGetOneChar parsed four hex digits at offset 8 without checking that offsets 6 and 7 hold a backslash and a 'u'. The string "\ud83dXXde00" decoded to U+1F600 and consumed 12 bytes, swallowing the two unrelated characters. sceNpUtilJsonUnescape already makes that check. A high surrogate not followed by \u now takes the same failure path as an invalid low surrogate. --- core/libs/prx/libSceNpCommon/Strings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpCommon/Strings.cpp b/core/libs/prx/libSceNpCommon/Strings.cpp index 8a2ec4ba0c..b0b75f8720 100644 --- a/core/libs/prx/libSceNpCommon/Strings.cpp +++ b/core/libs/prx/libSceNpCommon/Strings.cpp @@ -557,7 +557,7 @@ int APS5_VABI sceNpUtilJsonGetOneChar(char* dst, std::size_t dstSize, const char } else { used = JSON_SURROGATE_PAIR_LENGTH; std::uint32_t low = 0; - if (srcLength < JSON_SURROGATE_PAIR_LENGTH || ParseUtf16Unit(src + 8, &low) < 0 + if (srcLength < JSON_SURROGATE_PAIR_LENGTH || src[6] != '\\' || src[7] != 'u' || ParseUtf16Unit(src + 8, &low) < 0 || (low & SURROGATE_MASK) != LOW_SURROGATE) { status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; } else { From cefb89fceb75720f611fea142c381b8c640ffa77 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:24:44 +0000 Subject: [PATCH 649/702] fix(libSceNpCommon): let sceNpUtilJsonUnescape size a query with a null destination The final room check ran even when dst was null, so a size query such as (nullptr, 0, src, n, &length) returned NP_UTIL_ERROR_BUFFER_TOO_SMALL and never wrote the required length, although the per-character checks in the same function already skip the comparison for a null destination. The check now applies only when there is a destination. --- core/libs/prx/libSceNpCommon/Strings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpCommon/Strings.cpp b/core/libs/prx/libSceNpCommon/Strings.cpp index b0b75f8720..bda070c212 100644 --- a/core/libs/prx/libSceNpCommon/Strings.cpp +++ b/core/libs/prx/libSceNpCommon/Strings.cpp @@ -523,7 +523,7 @@ int APS5_VABI sceNpUtilJsonUnescape(char* dst, std::size_t dstSize, const char* emit(encoded, length, JSON_SURROGATE_PAIR_LENGTH); } ++count; - if (count > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (dst != nullptr && count > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; if (dstLength != nullptr) *dstLength = count; if (out != nullptr) *out = '\0'; return status; From e6083ad5c5e9e790b4e8bd45771491440d541cd7 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:25:02 +0000 Subject: [PATCH 650/702] test(libSceNpCommon): escape into a filled buffer, size a null-destination query, split a surrogate pair sceNpUtilJsonEscape has to stay inside a partly filled 16 byte buffer and refuse one without a terminator, sceNpUtilJsonUnescape has to report the length of a query with a null destination and size 0, and sceNpUtilJsonGetOneChar has to reject a high surrogate that is not followed by a second unicode escape. --- core/libs/tests/GuestNpCommonRuntime.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestNpCommonRuntime.cpp b/core/libs/tests/GuestNpCommonRuntime.cpp index 1747776629..9d646757ef 100644 --- a/core/libs/tests/GuestNpCommonRuntime.cpp +++ b/core/libs/tests/GuestNpCommonRuntime.cpp @@ -259,6 +259,16 @@ void TestJsonText() { Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), "yz", 2) == 0 && Equals(escaped, "xyz"), "escaping appends to the buffer"); Require(sceNpUtilJsonEscape(escaped, 6, "a", 1) == NpUtilBufferTooSmall, "escaping needs room for one escape"); Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), nullptr, 1) == NpUtilInvalid, "escaping needs a source"); + struct Guarded { + char buffer[16]; + char canary[16]; + } guarded{}; + std::strcpy(guarded.buffer, "0123456789"); + Require(sceNpUtilJsonEscape(guarded.buffer, sizeof(guarded.buffer), "\n\n\n", 3) == NpUtilBufferTooSmall && guarded.canary[0] == 0, + "escaping into a partly filled buffer counts the text already in it"); + std::memset(guarded.buffer, 'x', sizeof(guarded.buffer)); + Require(sceNpUtilJsonEscape(guarded.buffer, sizeof(guarded.buffer), "a", 1) == NpUtilBufferTooSmall && guarded.canary[0] == 0, + "escaping into a buffer without a terminator is refused"); char text[32] = {}; std::size_t length = 0; @@ -271,6 +281,8 @@ void TestJsonText() { "a bad unicode escape is copied and reported"); Require(sceNpUtilJsonUnescape(nullptr, sizeof(text), "abc", 3, &length) == 0 && length == 4, "a null buffer reports the size"); Require(sceNpUtilJsonUnescape(text, 3, "abc", 3, &length) == NpUtilBufferTooSmall, "unescaping needs room for the terminator"); + length = 0; + Require(sceNpUtilJsonUnescape(nullptr, 0, "abc", 3, &length) == 0 && length == 4, "a size query with no buffer and no size reports the size"); char one[4] = {}; std::size_t consumed = 0; @@ -284,6 +296,8 @@ void TestJsonText() { "a cut UTF-8 character reports its full length"); Require(sceNpUtilJsonGetOneChar(one, 1, "\xC3\xA9", 2, &length, &consumed) == NpUtilBufferTooSmall && length == 2, "a short buffer still reports the decoded size"); + Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\\ud83dXXde00", 12, &length, &consumed) == NpUtilBufferTooSmall, + "a high surrogate without a second unicode escape is not combined"); const char* character = nullptr; const char* japanese = "\xE6\x97\xA5" "a"; From 9a12233a76b9b4600a7575ddece14580a3608d80 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Wed, 28 Oct 2026 04:24:20 +0000 Subject: [PATCH 651/702] docs(libkernel): list sceKernelDebugRaiseException and its release-mode form as silent stubs Both log their arguments and the caller and return. Note what the titles' bundled libc.prx passes them and that three of its sceKernelDebugRaiseException calls are followed by ud2 in every build checked, so the guest reaches that ud2 here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 315904e281..5b6c0d1286 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -7,6 +7,7 @@ - [scePthreadMutexattrSetprotocol, scePthreadMutexattrGetprotocol, scePthreadMutexattrSetprioceiling, scePthreadMutexattrGetprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - `scePthreadMutexattrSetprotocol` accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not 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 - [pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol, 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 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A payload process on a PS5 (firmware 13.60) accepts protocols 0 to 2, starts the ceiling at 767 when protocol 2 is set, accepts ceilings 256 to 767 and returns `EINVAL` for other protocols and ceilings and for a null, uninitialized or destroyed attribute; a null output of `pthread_mutexattr_getprotocol` crashed it. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority - [sceCoredumpSetUserDataType](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Uxqkdta7wEg`) - `(int type)` follows the PS5 13.60 `libkernel.sprx` at 0x138B0: the sign-extended `type` goes to syscall 601 (command 29, third argument 0), and the raw result comes back with no SCE code mapping, so a kernel error is -1 with `errno` set. Which types the kernel accepts is not measured. No dump is produced here, so every type returns 0. Palworld (PPSA23126, #2598) and Hello Neighbor (PPSA35154, discussion #6991) import it; #4144 left it throwing because no signature was known. PPSA02225 also imports it: its one call (at 0x6a1b2) passes 1 and does not read the result. +- [sceKernelDebugRaiseException and sceKernelDebugRaiseExceptionOnReleaseMode](../../../core/libs/prx/libkernel/System/src/Exception.cpp) (libkernel) log their two arguments and the caller, then return; what the console does with them is not measured. The `sce_module/libc.prx` of nine titles checked (PPSA01494, PPSA02225, PPSA03984, PPSA18194, PPSA25286, PPSA30839, PPSA31162, PPSA34502 and PPSA36591, seven builds) calls them at 15 to 17 sites, nearly all with a `0xa002xxxx` code in the first argument and 0 in the second. In every build, three `sceKernelDebugRaiseException` calls are followed directly by `ud2`: libc does not expect them to return, so here the guest reaches that `ud2` right after the log line. - [pthread_rwlockattr_settype_np](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - stores types 1 and 2 through `scePthreadRwlockattrSettype` and returns 0, but the type is not applied: `scePthreadRwlockInit` ignores its attribute. Any other type, 0 included, and a null attribute or object return `EINVAL` and store nothing, as in [BunnyFl's measurements](https://github.com/boykopovar/AnyPS5/pull/9632#issuecomment-6363402814) of firmware 13.42's libkernel. Only the name is public; the `(attr, int)` signature follows the SCE twin and shadPS4, which also registers it as `scePthreadRwlockattrSettype` - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, where a payload process on a CFI-2014 PS5 (firmware 13.60) got 0x840f80, 1 and 800000000. The SoC id may differ between console models, and the CPU mode of a title may differ from a payload's (shadPS4 takes the PS4's from the param.sfo flags), so neither is copied. `sceKernelGetMainSocId` ignores a pointer argument there, as here. PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`) - [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 From 987a78dd13995aacf5f919dfc12f7fbb953b0bd1 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Wed, 28 Oct 2026 04:20:00 +0000 Subject: [PATCH 652/702] test(libkernel): cover scePthreadRwlockTryrdlock and Trywrlock on a live lock Crimson Desert calls scePthreadRwlockTryrdlock, and its only test is on a destroyed lock. On a lock made by scePthreadRwlockInit: two readers take it, a try for the write lock is busy while either holds it, the write lock is taken once both left, a reader is then refused, and after the writer unlocks a reader takes it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestPthreadRwlock.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 1c1a34e71c..f57954b010 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -443,8 +443,26 @@ static void CheckSceAttrLifecycle() { Require(throws([] { scePthreadRwlockattrInit(nullptr); })); } +static void CheckSceTryLocks() { + PthreadRwlock lock = nullptr; + Require(scePthreadRwlockInit(&lock, nullptr, "sce try") == SCE_OK && lock != nullptr); + Require(scePthreadRwlockTryrdlock(&lock) == SCE_OK); + Require(scePthreadRwlockTryrdlock(&lock) == SCE_OK); + Require(scePthreadRwlockTrywrlock(&lock) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadRwlockUnlock(&lock) == SCE_OK); + Require(scePthreadRwlockTrywrlock(&lock) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadRwlockUnlock(&lock) == SCE_OK); + Require(scePthreadRwlockTrywrlock(&lock) == SCE_OK); + Require(scePthreadRwlockTryrdlock(&lock) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadRwlockUnlock(&lock) == SCE_OK); + Require(scePthreadRwlockTryrdlock(&lock) == SCE_OK); + Require(scePthreadRwlockUnlock(&lock) == SCE_OK); + Require(scePthreadRwlockDestroy(&lock) == SCE_OK); +} + int main() { CheckSceAttrLifecycle(); + CheckSceTryLocks(); CheckAttrSettype(); CheckAttrProcessSharing(); CheckAttrType(); From f1af2881337ccdb68f731a32e4c9dade618f5ed9 Mon Sep 17 00:00:00 2001 From: Tehshortbus <leiterdg@gmail.com> Date: Tue, 27 Oct 2026 21:18:10 -0700 Subject: [PATCH 653/702] chore(agc): log the storage writeback submit and done lines only with APS5_DEBUG_LOG writeBackLayers printed "StorageTexture writeback submit" and "StorageTexture writeback done" unconditionally around the blocking submit of every storage image writeback. In five GTA V (PPSA04264) runs they were 54 to 978 lines per run, 5 to 55 % of the log. They now use APS5_LOG_CHARS_OUT_DEBUG, like the other per-operation lines of the AGC driver, and are compiled out unless APS5_DEBUG_LOG is defined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- 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 6cd0ab7101..53ad0992b4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -6382,9 +6382,9 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) { context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 1, &toCopy, 1, &backToGeneral); CopyBuffer(context, commands, tiled.Handle(), 0, host.Handle(), 0, original.size()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); - APS5_LOG_CHARS_OUT("StorageTexture writeback submit"); + APS5_LOG_CHARS_OUT_DEBUG("StorageTexture writeback submit"); batch.SubmitAndWait(); - APS5_LOG_CHARS_OUT("StorageTexture writeback done"); + APS5_LOG_CHARS_OUT_DEBUG("StorageTexture writeback done"); std::uint64_t storedBytes = 0; for (const auto& [from, to] : stores) storedBytes += to - from; countStorageWriteBack(drawn.rect ? storedBytes : guestBytes, false, drawn.drawn); From cc7276f09c4e2ec3f67382b1fa1654a6dc116ff6 Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:48:41 +0000 Subject: [PATCH 654/702] fix(tools): report import_audit symbolic link loops as failures --- tools/import_audit.py | 8 +++++--- tools/tests/test_import_audit.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 93aa0a865f..f15190730f 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -835,7 +835,9 @@ def attach_names(records, names): def write_json(path, result): if path.is_dir(): raise AuditError(f"{path}: JSON output is a directory") - destination = path.resolve() + destination = Path(os.path.realpath(path)) + if destination.is_symlink(): + raise AuditError(f"{path}: JSON output is a symbolic link loop") with tempfile.TemporaryDirectory(dir=destination.parent, prefix=destination.name + ".", suffix=".tmp") as directory: temporary = Path(directory) / destination.name try: @@ -974,8 +976,8 @@ def main(argv=None): inputs += [file for directory in args.modules for file in directory_files(directory, "*")] inputs += list(Path(args.source).rglob("*.cpp")) inputs += [file for file in Path(args.source).glob("*/CMakeLists.txt") if file.is_file()] - output = args.json.resolve() - if any(output == path.resolve() or (args.json.exists() and args.json.samefile(path)) for path in inputs): + output = Path(os.path.realpath(args.json)) + if any(output == Path(os.path.realpath(path)) or (args.json.exists() and args.json.samefile(path)) for path in inputs): raise AuditError(f"{args.json}: JSON output would overwrite an input file") registries = [(registry, read_registry(registry)) for registry in args.registries] imports = [entry for _, entries in registries for entry in entries] diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 1cd1d255bf..1496fea090 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -2336,6 +2336,23 @@ class ImportAuditTests(unittest.TestCase): self.assertIn("input", err) self.assertEqual(registry.read_bytes(), before) + def test_symbolic_link_loops_are_reported_as_failures(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + loop = work.root / "loop.json" + try: + loop.symlink_to(loop.name) + except (OSError, NotImplementedError): + self.skipTest("symbolic links are unavailable") + for arguments in ((registry, "--json", str(loop)), (loop, "--json", str(work.root / "audit.json"))): + with self.subTest(arguments=arguments): + code, out, err = work.run(*arguments) + self.assertEqual((code, out), (2, "")) + self.assertTrue(err.startswith("FAIL: "), err) + self.assertFalse((work.root / "audit.json").exists()) + def test_json_output_preserves_shared_source_cmake_inputs(self): work = self.work name = "scePending" From 126b7b98a0416ac66ffd18a5665d776444f304dd Mon Sep 17 00:00:00 2001 From: Michael Krumland-Dunning <mkrumland-dunning@ucsb.edu> Date: Tue, 27 Oct 2026 21:43:43 -0700 Subject: [PATCH 655/702] fix(shader): translate f32 fract, floor, ceil and frexp_mant in f32 denormal modes 1 and 2 All four threw "f32 denormal mode N is not implemented" whenever FLOAT_MODE kept only one side of the f32 denormals. On RDNA2 (gfx1036), in every f32 round mode and at IEEE 0 and 1, mode 2 gives the results of mode 0 and mode 1 those of mode 3 with a denormal result flushed, which only v_fract_f32 of a positive denormal produces. The translation now matches all 118 measured inputs in modes 1 and 2. Mode 1 of the three that run host float operations on a kept denormal needs DenormPreserve 32, as v_min_f32 does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../execution/F32FractFloorDenormalModes.cpp | 254 ++++++++++++++++++ .../Translation/src/VectorInstructions.cpp | 10 + docs/dev/debt/shader.md | 1 + 3 files changed, 265 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F32FractFloorDenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32FractFloorDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32FractFloorDenormalModes.cpp new file mode 100644 index 0000000000..a07fcb0547 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32FractFloorDenormalModes.cpp @@ -0,0 +1,254 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <cstdint> +#include <cstdio> +#include <iostream> +#include <optional> +#include <span> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{}; +alignas(256) std::array<std::uint32_t, Threads * Results> Output{}; + +alignas(256) constexpr std::array<std::uint32_t, 11> Code{ + 0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e144104u, 0x7e164904u, 0x7e184504u, 0x7e1a8104u, + 0xe0781000u, 0x80010a01u, 0xbf810000u, +}; + +constexpr const char* Names[Results] = {"v_fract_f32", "v_floor_f32", "v_ceil_f32", "v_frexp_mant_f32"}; + +constexpr std::uint32_t Values[Threads] = { + 0x00000000u, 0x00000001u, 0x00000002u, 0x00000003u, 0x00400000u, 0x007ffffeu, 0x007fffffu, 0x00800000u, + 0x00800001u, 0x00ffffffu, 0x01000000u, 0x0c000000u, 0x33ffffffu, 0x34000000u, 0x3effffffu, 0x3f000000u, + 0x3f7fffffu, 0x3f800000u, 0x3fc00000u, 0x40490fdbu, 0x4b000000u, 0x4b7fffffu, 0x7f7fffffu, 0x7f800000u, + 0x7fc00000u, 0x7fc12345u, 0x80000000u, 0x80000001u, 0x80000002u, 0x80000003u, 0x80400000u, 0x807ffffeu, + 0x807fffffu, 0x80800000u, 0x80800001u, 0x80ffffffu, 0x81000000u, 0x8c000000u, 0xb3ffffffu, 0xb4000000u, + 0xbeffffffu, 0xbf000000u, 0xbf7fffffu, 0xbf800000u, 0xbfc00000u, 0xc0490fdbu, 0xcb000000u, 0xcb7fffffu, + 0xff7fffffu, 0xff800000u, 0xffc00000u, 0xffc12345u, 0x804fbb3fu, 0x805ef2e1u, 0x0065c8e8u, 0x806b9087u, + 0x0063529du, 0x0053119au, 0x007350b6u, 0x800e7d89u, 0x003c0a75u, 0x0030bcadu, 0x004976e8u, 0x0001adbau +}; +constexpr std::uint32_t Mode1Expected[Threads][Results] = { + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f400000u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f7ffffcu}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f7ffffeu}, + {0x00800000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x00800001u, 0x00000000u, 0x3f800000u, 0x3f000001u}, + {0x00ffffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu}, + {0x01000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x0c000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x33ffffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu}, + {0x34000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x3effffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu}, + {0x3f000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x3f7fffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu}, + {0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f000000u}, + {0x3f000000u, 0x3f800000u, 0x40000000u, 0x3f400000u}, + {0x3e10fdb0u, 0x40400000u, 0x40800000u, 0x3f490fdbu}, + {0x00000000u, 0x4b000000u, 0x4b000000u, 0x3f000000u}, + {0x00000000u, 0x4b7fffffu, 0x4b7fffffu, 0x3f7fffffu}, + {0x00000000u, 0x7f7fffffu, 0x7f7fffffu, 0x3f7fffffu}, + {0xffc00000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf400000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf7ffffcu}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf7ffffeu}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000001u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf7fffffu}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f7ffffeu, 0xbf800000u, 0x80000000u, 0xbf7fffffu}, + {0x3f7ffffeu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f000000u, 0xbf800000u, 0x80000000u, 0xbf7fffffu}, + {0x3f000000u, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x33800000u, 0xbf800000u, 0x80000000u, 0xbf7fffffu}, + {0x00000000u, 0xbf800000u, 0xbf800000u, 0xbf000000u}, + {0x3f000000u, 0xc0000000u, 0xbf800000u, 0xbf400000u}, + {0x3f5bc094u, 0xc0800000u, 0xc0400000u, 0xbf490fdbu}, + {0x00000000u, 0xcb000000u, 0xcb000000u, 0xbf000000u}, + {0x00000000u, 0xcb7fffffu, 0xcb7fffffu, 0xbf7fffffu}, + {0x00000000u, 0xff7fffffu, 0xff7fffffu, 0xbf7fffffu}, + {0xffc00000u, 0xff800000u, 0xff800000u, 0xff800000u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u}, + {0xffc12345u, 0xffc12345u, 0xffc12345u, 0xffc12345u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf1f767eu}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf3de5c2u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f4b91d0u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf57210eu}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f46a53au}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f262334u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f66a16cu}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf67d890u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f7029d4u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f42f2b4u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f12edd0u}, + {0x00000000u, 0x00000000u, 0x3f800000u, 0x3f56dd00u} +}; +constexpr std::uint32_t Mode2Expected[Threads][Results] = { + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x00800001u, 0x00000000u, 0x3f800000u, 0x3f000001u}, + {0x00ffffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu}, + {0x01000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x0c000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x33ffffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu}, + {0x34000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x3effffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu}, + {0x3f000000u, 0x00000000u, 0x3f800000u, 0x3f000000u}, + {0x3f7fffffu, 0x00000000u, 0x3f800000u, 0x3f7fffffu}, + {0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f000000u}, + {0x3f000000u, 0x3f800000u, 0x40000000u, 0x3f400000u}, + {0x3e10fdb0u, 0x40400000u, 0x40800000u, 0x3f490fdbu}, + {0x00000000u, 0x4b000000u, 0x4b000000u, 0x3f000000u}, + {0x00000000u, 0x4b7fffffu, 0x4b7fffffu, 0x3f7fffffu}, + {0x00000000u, 0x7f7fffffu, 0x7f7fffffu, 0x3f7fffffu}, + {0xffc00000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000001u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf7fffffu}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f7fffffu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f7ffffeu, 0xbf800000u, 0x80000000u, 0xbf7fffffu}, + {0x3f7ffffeu, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x3f000000u, 0xbf800000u, 0x80000000u, 0xbf7fffffu}, + {0x3f000000u, 0xbf800000u, 0x80000000u, 0xbf000000u}, + {0x33800000u, 0xbf800000u, 0x80000000u, 0xbf7fffffu}, + {0x00000000u, 0xbf800000u, 0xbf800000u, 0xbf000000u}, + {0x3f000000u, 0xc0000000u, 0xbf800000u, 0xbf400000u}, + {0x3f5bc094u, 0xc0800000u, 0xc0400000u, 0xbf490fdbu}, + {0x00000000u, 0xcb000000u, 0xcb000000u, 0xbf000000u}, + {0x00000000u, 0xcb7fffffu, 0xcb7fffffu, 0xbf7fffffu}, + {0x00000000u, 0xff7fffffu, 0xff7fffffu, 0xbf7fffffu}, + {0xffc00000u, 0xff800000u, 0xff800000u, 0xff800000u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u}, + {0xffc12345u, 0xffc12345u, 0xffc12345u, 0xffc12345u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u} +}; + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((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::optional<std::string> Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) Input[tid * Inputs] = Values[tid]; + Output.fill(0xdeadbeefu); + std::vector<std::uint32_t> userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span<const std::uint32_t> code(Code); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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; + std::optional<decltype(ShaderRecompiler::Recompile(request))> result; + try { + result.emplace(ShaderRecompiler::Recompile(request)); + } catch (const std::exception& error) { + return std::string(error.what()); + } + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device.WaitIdle(); + return std::nullopt; +} + +void Check(std::uint32_t denormals, bool ieee) { + const auto& expected = denormals == 1u ? Mode1Expected : Mode2Expected; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t i = 0; i < Results; ++i) { + const std::uint32_t actual = Output[tid * Results + i]; + Require(actual == expected[tid][i], std::string(Names[i]) + " f32 denormal mode " + std::to_string(denormals) + (ieee ? ", IEEE=1" : ", IEEE=0") + " of " + Hex(Values[tid]) + " is " + Hex(actual) + ", expected " + Hex(expected[tid][i])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool keepsDenormals = device->Target().float32DenormPreserve; + std::uint32_t checked = 0u; + for (const std::uint32_t denormals : {1u, 2u}) { + for (const bool ieee : {false, true}) { + const ShaderRecompiler::ShaderFloatMode floatMode{0xc0u | denormals << 4u, true, ieee, false}; + const auto refusal = Run(*device, floatMode); + if (refusal) { + Require(denormals == 1u && !keepsDenormals && refusal->find("needs DenormPreserve 32") != std::string::npos, "FLOAT_MODE " + Hex(floatMode.floatMode) + ": " + *refusal); + continue; + } + ++checked; + Check(denormals, ieee); + } + } + std::printf("f32 fract, floor, ceil and frexp_mant denormal mode 1 and 2 tests passed (%u of 4 modes run; the others need DenormPreserve 32)\n", checked); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 20a45cee03..7debd03e3e 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -77,6 +77,16 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VFloorF32: case RdnaOpcode::VCeilF32: case RdnaOpcode::VFrexpMantF32: + if (floatMode.has_value() && ((floatMode->floatMode >> 4u) & 3u) == 2u) return 1u; + if (floatMode.has_value() && ((floatMode->floatMode >> 4u) & 3u) == 1u) { + if (inst.op == RdnaOpcode::VFrexpMantF32) return 0u; + if (!float32DenormPreserve || float32FlushToZero) { + throw std::runtime_error("f32 denormal mode 1 at pc " + std::to_string(inst.programCounter) + " needs DenormPreserve 32, which the device does not offer for this shader"); + } + return 2u; + } + flush = 3u; + break; case RdnaOpcode::VSinF32: case RdnaOpcode::VCosF32: flush = 3u; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 9bd5810262..3a96bbca22 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -138,6 +138,7 @@ - [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. - [v_add_f32, v_sub_f32, v_subrev_f32, v_mul_f32 and v_mul_legacy_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) throw with a directed f32 round mode, as the host rounds these sums and products to nearest even (a difference that modes 0 and 3 also have then), and when the module cannot declare `DenormPreserve 32` (no `shaderDenormPreserveFloat32`, or the hardware f32 flush in use); the PS5's gfx1013 is not measured. Otherwise mode 1 keeps denormal inputs and flushes a result below 2^-126 to a zero of its sign (a product only when it stays below 2^-126 after rounding to 24 bits with an unbounded exponent), and mode 2 flushes denormal inputs and keeps a denormal result. Measured on RDNA2 (gfx1030, RX 6800) over 1,296 operand pairs, 896 products around 2^-126 and 1,632 with a constant factor, at IEEE 0 and 1 in every f32 denormal mode, with DX10_CLAMP=1, f16 denormals kept and FP16_OVFL=0. - [f32 min and max in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32`, `v_med3_f32`) throw in mode 1 when the module cannot declare `DenormPreserve 32` (no `shaderDenormPreserveFloat32`, or the hardware f32 flush in use), and mode 3 keeps depending on the driver there; the PS5's gfx1013 is not measured. They follow only bit 0 of the `FLOAT_MODE` f32 denormal field: mode 2 flushes denormal inputs to zeros of their sign as mode 0 does, and mode 1 keeps them and returns a selected denormal unflushed as mode 3 does. Measured on RDNA2 (gfx1030, RX 6800) over the 2,744 operand triples of ±0, ±2^-149, ±0x007fffff, ±2^-126, ±1, ±infinity, a quiet and a signaling NaN at IEEE 0 and 1 in each f32 denormal mode, round to nearest even, DX10_CLAMP=1, f16 denormals kept, FP16_OVFL=0: modes 1 and 3 give identical results, and so do modes 2 and 0. +- [v_fract_f32, v_floor_f32, v_ceil_f32 and v_frexp_mant_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) throw in mode 1, bar `v_frexp_mant_f32`, when the module cannot declare `DenormPreserve 32`; `v_sin_f32` and `v_cos_f32` still throw in both modes, and the PS5's gfx1013 is not measured. Mode 2 flushes a denormal input to a zero of its sign as mode 0 does. Mode 1 keeps it as mode 3 does (`v_floor_f32` of -2^-149 is -1) and flushes a denormal result to a zero of its sign, which only `v_fract_f32` of a positive denormal gives. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31 through HIP) over 118 inputs (zeros, denormals, minimum normals, values around 0.5, 1 and 2^23, the largest finite value, infinities, quiet and signaling NaNs, of both signs, and 64 random denormals) at IEEE 0 and 1 in every f32 denormal and round mode: modes 2 and 0 give identical results, and modes 1 and 3 differ only in those flushed results. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked - [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. From 254d7b416f352bd162aae9d29bcf7b9cc2f8cfb4 Mon Sep 17 00:00:00 2001 From: Michael Krumland-Dunning <mkrumland-dunning@ucsb.edu> Date: Tue, 27 Oct 2026 21:48:55 -0700 Subject: [PATCH 656/702] fix(shader): translate the fused f32 fma forms in f32 denormal mode 2 v_fma_f32, v_fmac_f32, v_fmamk_f32 and v_fmaak_f32 threw "f32 denormal mode 2 is not implemented" whenever FLOAT_MODE flushed f32 denormal inputs but kept denormal results. On RDNA2 (gfx1036) at round to nearest even, mode 2 flushes the inputs and rounds the fused result once with gradual underflow, which the host's Fma gives under DenormPreserve 32. The translation now matches all 5,596 measured triples for the four forms at IEEE 0 and 1. Mode 1 keeps throwing: there the hardware flushes a result that is tiny after rounding with an unbounded exponent even when the addend is nonzero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../tests/execution/F32FmaDenormalMode2.cpp | 241 ++++++++++++++++++ .../Translation/src/VectorInstructions.cpp | 9 +- docs/dev/debt/shader.md | 1 + 3 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F32FmaDenormalMode2.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32FmaDenormalMode2.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32FmaDenormalMode2.cpp new file mode 100644 index 0000000000..58e6442cc3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32FmaDenormalMode2.cpp @@ -0,0 +1,241 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <cstdint> +#include <cstdio> +#include <iostream> +#include <optional> +#include <span> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{}; +alignas(256) std::array<std::uint32_t, Threads * Results> Output{}; + +alignas(256) constexpr std::array<std::uint32_t, 15> Code{ + 0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd54b000au, 0x041a0b04u, 0x7e160306u, 0x56160b04u, + 0x58180d04u, 0x3f7fffffu, 0x5a1a0b04u, 0x80000001u, 0xe0781000u, 0x80010a01u, 0xbf810000u, +}; + +constexpr const char* Names[Results] = {"v_fma_f32 v10, v4, v5, v6", "v_fmac_f32 v11, v4, v5", "v_fmamk_f32 v12, v4, 0x3f7fffff, v6", "v_fmaak_f32 v13, v4, v5, 0x80000001"}; + +constexpr std::uint32_t Rows[Threads][3] = { + {0x00000000u, 0x7f800000u, 0x3f000000u}, + {0x00800000u, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x00000000u, 0x80000000u}, + {0x00800000u, 0x00000000u, 0x00000001u}, + {0x00800000u, 0x00000000u, 0x807fffffu}, + {0x00800000u, 0x00000000u, 0x00400000u}, + {0x00800000u, 0x00000000u, 0x80800001u}, + {0x00800000u, 0x80000000u, 0x00000000u}, + {0x00800000u, 0x80000000u, 0x80000000u}, + {0x00800000u, 0x00400000u, 0x00000001u}, + {0x00800000u, 0x00800000u, 0x80000000u}, + {0x00800000u, 0x3f800000u, 0x80800001u}, + {0x00800000u, 0x3f7fffffu, 0x00000000u}, + {0x00800000u, 0x3f7fffffu, 0x80000000u}, + {0x00800000u, 0x3f7fffffu, 0x00000001u}, + {0x00800000u, 0x3f7fffffu, 0x807fffffu}, + {0x00800000u, 0x3f7fffffu, 0x00400000u}, + {0x00800000u, 0x3f7fffffu, 0x00800000u}, + {0x00800000u, 0x3f7fffffu, 0x80800001u}, + {0x00800000u, 0x3f7fffffu, 0x3f800000u}, + {0x00800000u, 0x3f7fffffu, 0xbf800000u}, + {0x00800000u, 0x3f7fffffu, 0x3f7fffffu}, + {0x00800000u, 0x3f7fffffu, 0x3f000000u}, + {0x00800000u, 0x3f7fffffu, 0x1f800000u}, + {0x00800000u, 0x3f7fffffu, 0x9fffffffu}, + {0x00800000u, 0x3f7fffffu, 0x20000000u}, + {0x00800000u, 0x3f000000u, 0x00000000u}, + {0x00800000u, 0x3f000000u, 0x80000000u}, + {0x00800000u, 0x3f000000u, 0x00000001u}, + {0x00800000u, 0x3f000000u, 0x807fffffu}, + {0x00800000u, 0x3f000000u, 0x00400000u}, + {0x00800000u, 0x3f000000u, 0x80800001u}, + {0x00800000u, 0x1f800000u, 0x80000000u}, + {0x80800001u, 0x3f800000u, 0x00800000u}, + {0x80800001u, 0x3f000000u, 0x7f800000u}, + {0x3f800000u, 0x80800001u, 0x00800000u}, + {0x3f800000u, 0x7fc00000u, 0x80800001u}, + {0x3f7fffffu, 0xbf800000u, 0x3f000000u}, + {0x1f800000u, 0x3f800000u, 0x20000000u}, + {0x9fffffffu, 0x00000001u, 0x80000000u}, + {0x9fffffffu, 0x20000000u, 0x00400000u}, + {0x20000000u, 0x3f800000u, 0x00000000u}, + {0x20000000u, 0x9fffffffu, 0x20000000u}, + {0x20000000u, 0x9fffffffu, 0x7fc00000u}, + {0x7f800000u, 0x00400000u, 0x00000001u}, + {0xa041753bu, 0x1f0d790au, 0x80000000u}, + {0x9efedcd2u, 0x9ff05395u, 0x80000000u}, + {0x1f47e4fdu, 0x1eb6f5c4u, 0x80000000u}, + {0x9e9e1749u, 0x1f7a0022u, 0x8086d61eu}, + {0x9fc47dc5u, 0x1f7a84eeu, 0x80ad97ffu}, + {0x9e398753u, 0x20a52a9au, 0x809fead7u}, + {0x1ee684f0u, 0x204b1dd4u, 0x00000000u}, + {0x1e8933deu, 0x205cc4a7u, 0x00000000u}, + {0x1f19a9f4u, 0x9fbc243cu, 0x80000000u}, + {0x9eb3c398u, 0x9f630477u, 0x00000000u}, + {0x1e32d290u, 0x1fa0e0b3u, 0x00000000u}, + {0x1f074330u, 0x9e704ff0u, 0x00000000u}, + {0x1ea63ad2u, 0x1e7c9dc9u, 0x806cc811u}, + {0x9f42587bu, 0xa0dd6a0cu, 0x80000000u}, + {0x202187c6u, 0x1eb02829u, 0x80000000u}, + {0x1fe6f5d7u, 0x208cc5a7u, 0x80df394au}, + {0x1f2af88cu, 0x9e6ce54fu, 0x80000000u}, + {0x1eeac417u, 0x9e8ead57u, 0x00000000u}, + {0x9e406f2cu, 0x20e09d8du, 0x004226bcu} +}; +constexpr std::uint32_t Expected[Threads][Results] = { + {0xffc00000u, 0xffc00000u, 0x3f000000u, 0xffc00000u}, + {0x00000000u, 0x00000000u, 0x00800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00800000u, 0x00000000u}, + {0x80800001u, 0x80800001u, 0x80000002u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00800000u, 0x80000000u}, + {0x80000000u, 0x80000000u, 0x00800000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x00800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00800000u, 0x00000000u}, + {0x80000001u, 0x80000001u, 0x80000002u, 0x00800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, + {0x01000000u, 0x01000000u, 0x01000000u, 0x00800000u}, + {0x80000002u, 0x80000002u, 0x80000002u, 0x00800000u}, + {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00800000u}, + {0xbf800000u, 0xbf800000u, 0xbf800000u, 0x00800000u}, + {0x3f7fffffu, 0x3f7fffffu, 0x3f7fffffu, 0x00800000u}, + {0x3f000000u, 0x3f000000u, 0x3f000000u, 0x00800000u}, + {0x1f800000u, 0x1f800000u, 0x1f800000u, 0x00800000u}, + {0x9fffffffu, 0x9fffffffu, 0x9fffffffu, 0x00800000u}, + {0x20000000u, 0x20000000u, 0x20000000u, 0x00800000u}, + {0x00400000u, 0x00400000u, 0x00800000u, 0x00400000u}, + {0x00400000u, 0x00400000u, 0x00800000u, 0x00400000u}, + {0x00400000u, 0x00400000u, 0x00800000u, 0x00400000u}, + {0x00400000u, 0x00400000u, 0x00800000u, 0x00400000u}, + {0x00400000u, 0x00400000u, 0x00800000u, 0x00400000u}, + {0x80400001u, 0x80400001u, 0x80000002u, 0x00400000u}, + {0x00000000u, 0x00000000u, 0x00800000u, 0x00000000u}, + {0x80000001u, 0x80000001u, 0x80000000u, 0x80800001u}, + {0x7f800000u, 0x7f800000u, 0x7f800000u, 0x80400000u}, + {0x80000001u, 0x80000001u, 0x3f7fffffu, 0x80800001u}, + {0x7fc00000u, 0x7fc00000u, 0x3f7fffffu, 0x7fc00000u}, + {0xbefffffeu, 0xbefffffeu, 0x3fbfffffu, 0xbf7fffffu}, + {0x20400000u, 0x20400000u, 0x20400000u, 0x1f800000u}, + {0x80000000u, 0x80000000u, 0x9ffffffeu, 0x80000000u}, + {0x80800000u, 0x80800000u, 0x9ffffffeu, 0x80800000u}, + {0x20000000u, 0x20000000u, 0x1fffffffu, 0x20000000u}, + {0x20000000u, 0x20000000u, 0x20800000u, 0x80800000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x80800000u}, + {0xffc00000u, 0xffc00000u, 0x7f800000u, 0xffc00000u}, + {0x80357485u, 0x80357485u, 0xa041753au, 0x80357485u}, + {0x001de847u, 0x001de847u, 0x9efedcd1u, 0x001de847u}, + {0x0008edcbu, 0x0008edcbu, 0x1f47e4fcu, 0x0008edcbu}, + {0x80907c4bu, 0x80907c4bu, 0x9e9e1748u, 0x8009a62du}, + {0x80ddaa35u, 0x80ddaa35u, 0x9fc47dc4u, 0x80301236u}, + {0x80bdd79du, 0x80bdd79du, 0x9e398752u, 0x801decc6u}, + {0x002db992u, 0x002db992u, 0x1ee684efu, 0x002db992u}, + {0x001d947eu, 0x001d947eu, 0x1e8933ddu, 0x001d947eu}, + {0x801c3ba4u, 0x801c3ba4u, 0x1f19a9f3u, 0x801c3ba4u}, + {0x0009f699u, 0x0009f699u, 0x9eb3c397u, 0x0009f699u}, + {0x00070609u, 0x00070609u, 0x1e32d28fu, 0x00070609u}, + {0x8003f7cau, 0x8003f7cau, 0x1f07432fu, 0x8003f7cau}, + {0x00029021u, 0x00029021u, 0x1ea63ad1u, 0x00029021u}, + {0x00a816e4u, 0x00a816e4u, 0x9f42587au, 0x00a816e4u}, + {0x001bc9acu, 0x001bc9acu, 0x202187c5u, 0x001bc9acu}, + {0x001ec83du, 0x001ec83du, 0x1fe6f5d6u, 0x00fe0187u}, + {0x8004f1b2u, 0x8004f1b2u, 0x1f2af88bu, 0x8004f1b2u}, + {0x800416beu, 0x800416beu, 0x1eeac416u, 0x800416beu}, + {0x802a35edu, 0x802a35edu, 0x9e406f2bu, 0x802a35edu} +}; + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((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::optional<std::string> Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + std::vector<std::uint32_t> userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span<const std::uint32_t> code(Code); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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; + std::optional<decltype(ShaderRecompiler::Recompile(request))> result; + try { + result.emplace(ShaderRecompiler::Recompile(request)); + } catch (const std::exception& error) { + return std::string(error.what()); + } + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device.WaitIdle(); + return std::nullopt; +} + +void Check(bool ieee) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t i = 0; i < Results; ++i) { + const std::uint32_t actual = Output[tid * Results + i]; + Require(actual == Expected[tid][i], std::string(Names[i]) + " f32 denormal mode 2" + (ieee ? ", IEEE=1" : ", IEEE=0") + " with v4 " + Hex(Rows[tid][0]) + ", v5 " + Hex(Rows[tid][1]) + ", v6 " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(Expected[tid][i])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool keepsDenormals = device->Target().float32DenormPreserve; + std::uint32_t checked = 0u; + for (const bool ieee : {false, true}) { + const ShaderRecompiler::ShaderFloatMode floatMode{0xe0u, true, ieee, false}; + const auto refusal = Run(*device, floatMode); + if (refusal) { + Require(!keepsDenormals && refusal->find("needs DenormPreserve 32") != std::string::npos, "FLOAT_MODE " + Hex(floatMode.floatMode) + ": " + *refusal); + continue; + } + ++checked; + Check(ieee); + } + std::printf("f32 fma denormal mode 2 tests passed (%u of 2 IEEE modes run; the others need DenormPreserve 32)\n", checked); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 20a45cee03..b76593d7b9 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -85,7 +85,14 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VMadmkF32: case RdnaOpcode::VMadakF32: case RdnaOpcode::VFmaF32: - flush = roundsProductSeparately(inst) ? 0u : 3u; + if (roundsProductSeparately(inst)) break; + if (floatMode.has_value() && (floatMode->floatMode & 3u) == 0u && ((floatMode->floatMode >> 4u) & 3u) == 2u) { + if (!float32DenormPreserve || float32FlushToZero) { + throw std::runtime_error("f32 denormal mode 2 at pc " + std::to_string(inst.programCounter) + " needs DenormPreserve 32, which the device does not offer for this shader"); + } + return 1u; + } + flush = 3u; break; case RdnaOpcode::VCmpEqF32: case RdnaOpcode::VCmpGeF32: diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 9bd5810262..65fa0a440e 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -134,6 +134,7 @@ - [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_rcp_f32 and v_rcp_iflag_f32 with `APS5_EXACT_RCP`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp) return the hardware reciprocal for normal inputs with normal results: a cubic over 32 mantissa segments, the linear term rounded to nearest even, plus 2,761 one-ULP corrections. It matches all 2^23 mantissas of an RDNA2 (gfx1031) sweep; inputs or results that are not normal keep the driver's 1/x. It is off by default because the coefficient and correction tables add about 13k SPIR-V words to each shader that uses it. The switch also reaches v_rcp_f16 (rounded from the f32 reciprocal) and the pixel shader's position W input. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. +- [v_fma_f32, v_fmac_f32, v_fmamk_f32 and v_fmaak_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) throw in mode 1, with a directed f32 round mode, and in mode 2 when the module cannot declare `DenormPreserve 32`; the PS5's gfx1013 is not measured. Mode 2 flushes denormal inputs to zeros of their sign and rounds the fused result once with gradual underflow, the host's `Fma` under `DenormPreserve 32`. Mode 1 keeps the inputs and flushes a result that is tiny after rounding to 24 bits with an unbounded exponent, even with a nonzero addend (`0x807fffff * 0x3f7fffff + 0x80000001` gives `0x80000000`, the fused result is `0x80800000`); that check is not implemented. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31 through HIP) over 5,596 triples (16^3 of 16 boundary values, and 1,500 random ones with products near 2^-126) at IEEE 0 and 1 in every f32 denormal mode at round to nearest even. - [32-bit denormal execution mode](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler) - the hardware f32 flush that the startup probe chooses (`DenormFlushToZero 32`) is declared only in shaders whose `FLOAT_MODE` flushes f32 denormals (field 0, or no known float mode). A shader that keeps them on either side (field 1, 2 or 3) declares `DenormPreserve 32` when the device has `shaderDenormPreserveFloat32` with a `denormBehaviorIndependence` other than NONE; on other devices it gets the driver's default f32 denormal handling, which flushes the denormals it keeps on MoltenVK. - [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. - [v_add_f32, v_sub_f32, v_subrev_f32, v_mul_f32 and v_mul_legacy_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) throw with a directed f32 round mode, as the host rounds these sums and products to nearest even (a difference that modes 0 and 3 also have then), and when the module cannot declare `DenormPreserve 32` (no `shaderDenormPreserveFloat32`, or the hardware f32 flush in use); the PS5's gfx1013 is not measured. Otherwise mode 1 keeps denormal inputs and flushes a result below 2^-126 to a zero of its sign (a product only when it stays below 2^-126 after rounding to 24 bits with an unbounded exponent), and mode 2 flushes denormal inputs and keeps a denormal result. Measured on RDNA2 (gfx1030, RX 6800) over 1,296 operand pairs, 896 products around 2^-126 and 1,632 with a constant factor, at IEEE 0 and 1 in every f32 denormal mode, with DX10_CLAMP=1, f16 denormals kept and FP16_OVFL=0. From b934ab8af129eebe2af058ae427f046c376cc453 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:26:57 -0400 Subject: [PATCH 657/702] fix(libSceRudp): drain only the cancelled waits and release a wait's registration on every exit sceRudpPollCancel waited until no wait was registered, so a wait that started during the drain kept it blocked, forever for an infinite one. Waits registered when the cancel starts now move to a draining count, and the cancel returns once those have left. A scope guard releases each wait's registration on every exit, retaking the lock if the wait had dropped it, so an exception in the condition wait no longer leaves a registration that a later cancel can never drain. Reported by @MarcZX10, @thepoppfish, @adriadam10 and @oneandonlydean; the guard follows @thepoppfish's repair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/CMakeLists.txt | 8 ++ core/libs/prx/libSceRudp/Export.cpp | 30 ++++++- core/libs/tests/GuestRudpPoll.cpp | 33 +++++++ core/libs/tests/RudpPollWaitException.cpp | 104 ++++++++++++++++++++++ docs/dev/debt/libSceRudp.md | 2 +- 5 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/RudpPollWaitException.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ae4646d52b..95c9b0c70d 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -812,6 +812,14 @@ if(BUILD_TESTING) set_tests_properties(guest_rudp_poll PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>") endif() + if(WIN32) + add_test_executable(rudp_poll_wait_exception_tests tests/RudpPollWaitException.cpp + prx/libSceRudp/Export.cpp prx/libkernel/Time/TimedWait.cpp) + target_include_directories(rudp_poll_wait_exception_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + configure_windows_unwind(rudp_poll_wait_exception_tests) + add_test(NAME rudp_poll_wait_exception COMMAND rudp_poll_wait_exception_tests) + set_tests_properties(rudp_poll_wait_exception PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77) + endif() add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index adf738bd9e..41955952c5 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -92,6 +92,7 @@ struct RudpPoll { std::size_t capacity; std::map<int, uint16_t> entries; int waiters = 0; + int draining = 0; uint64_t cancels = 0; bool cancelPending = false; }; @@ -100,6 +101,28 @@ int g_next_poll = 1; std::map<int, RudpPoll> g_polls; TimedWait::Condition g_polls_changed; +struct RudpPollWaitRegistration { + int pollId; + uint64_t cancels; + std::unique_lock<std::mutex>& lock; + + ~RudpPollWaitRegistration() { + if (!lock.owns_lock()) { + lock.lock(); + } + const auto poll = g_polls.find(pollId); + if (poll == g_polls.end()) { + return; + } + if (poll->second.cancels == cancels) { + --poll->second.waiters; + } else { + --poll->second.draining; + } + g_polls_changed.NotifyAll(); + } +}; + bool valid_ctx(int ctx) { return g_ctx.count(ctx) != 0; } @@ -458,6 +481,7 @@ int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, std::size_t eve } const uint64_t cancels = poll->second.cancels; ++poll->second.waiters; + const RudpPollWaitRegistration registration{pollId, cancels, lk}; const auto woken = [pollId, cancels] { if (!g_inited) { return true; @@ -478,8 +502,6 @@ int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, std::size_t eve if (current == g_polls.end()) { return RUDP_ERROR_INVALID_POLL_ID; } - --current->second.waiters; - g_polls_changed.NotifyAll(); return signalled ? RUDP_ERROR_CANCELLED : 0; } @@ -535,13 +557,15 @@ int APS5_VABI sceRudpPollCancel(int pollId) { return 0; } ++poll->second.cancels; + poll->second.draining += poll->second.waiters; + poll->second.waiters = 0; g_polls_changed.NotifyAll(); g_polls_changed.Wait(lk, [pollId] { if (!g_inited) { return true; } const auto current = g_polls.find(pollId); - return current == g_polls.end() || current->second.waiters == 0; + return current == g_polls.end() || current->second.draining == 0; }); return 0; } diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp index 8ae670e24e..3d84a2db62 100644 --- a/core/libs/tests/GuestRudpPoll.cpp +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -3,6 +3,7 @@ #include <algorithm> #include <array> +#include <atomic> #include <chrono> #include <cstddef> #include <cstdint> @@ -139,6 +140,38 @@ int main() { Require(sceRudpPollDestroy(cancelPoll) == 0); Require(sceRudpPollCancel(cancelPoll) == invalidPoll); + const int latePoll = sceRudpPollCreate(1); + Require(latePoll > 0); + for (int round = 0; round < 40; ++round) { + std::array<RudpPollEvent, 4> earlyEvents{}; + std::array<RudpPollEvent, 4> lateEvents{}; + std::atomic<int> earlyResult{1}; + std::atomic<int> lateResult{1}; + std::atomic<bool> cancelReturned{false}; + std::thread earlyWaiter([&] { earlyResult = sceRudpPollWait(latePoll, earlyEvents.data(), eventCount, forever); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + std::thread canceller([&] { + Require(sceRudpPollCancel(latePoll) == 0); + cancelReturned = true; + }); + std::thread lateWaiter([&] { lateResult = sceRudpPollWait(latePoll, lateEvents.data(), eventCount, forever); }); + const auto cancelledAt = std::chrono::steady_clock::now(); + while (!cancelReturned && std::chrono::steady_clock::now() - cancelledAt < std::chrono::seconds(2)) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(cancelReturned); + canceller.join(); + while (earlyResult == 1 || lateResult == 1) { + Require(sceRudpPollCancel(latePoll) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + earlyWaiter.join(); + lateWaiter.join(); + Require(earlyResult == cancelled && lateResult == cancelled); + sceRudpPollWait(latePoll, events.data(), eventCount, 0); + Require(sceRudpPollWait(latePoll, events.data(), eventCount, 0) == 0); + } + Require(sceRudpPollDestroy(latePoll) == 0); + const int finitePoll = sceRudpPollCreate(1); Require(finitePoll > 0); int finiteResult = 0; diff --git a/core/libs/tests/RudpPollWaitException.cpp b/core/libs/tests/RudpPollWaitException.cpp new file mode 100644 index 0000000000..65b72077e2 --- /dev/null +++ b/core/libs/tests/RudpPollWaitException.cpp @@ -0,0 +1,104 @@ +#include "prx/libSceRudp/include/Api.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" +#include "SceTypes.hpp" + +#include <array> +#include <atomic> +#include <chrono> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <limits> +#include <new> +#include <stdexcept> +#include <thread> + +namespace { + +thread_local bool failAllocations = false; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +} + +void* operator new(std::size_t bytes) { + if (failAllocations) throw std::bad_alloc(); + if (void* memory = std::malloc(bytes == 0 ? 1 : bytes)) return memory; + throw std::bad_alloc(); +} + +void* operator new[](std::size_t bytes) { + return ::operator new(bytes); +} + +void operator delete(void* memory) noexcept { + std::free(memory); +} + +void operator delete(void* memory, std::size_t) noexcept { + std::free(memory); +} + +void operator delete[](void* memory) noexcept { + std::free(memory); +} + +void operator delete[](void* memory, std::size_t) noexcept { + std::free(memory); +} + +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* name) { + throw std::runtime_error(name); +} + +int main() { + if (TimedWait::Coarse()) return 77; + constexpr int cancelled = static_cast<int>(0x80770014u); + constexpr std::uint64_t forever = std::numeric_limits<std::uint64_t>::max(); + alignas(16) static std::array<unsigned char, 0x10000> pool{}; + Require(sceRudpInit_nid_postfix(pool.data(), static_cast<int>(pool.size())) == 0, "sceRudpInit"); + const int poll = sceRudpPollCreate(1); + Require(poll > 0, "sceRudpPollCreate"); + for (const std::uint64_t timeout : {forever, std::uint64_t{10000000}}) { + std::atomic<bool> threw{false}; + std::thread waiter([&] { + std::array<RudpPollEvent, 4> events{}; + failAllocations = true; + try { + sceRudpPollWait(poll, events.data(), static_cast<std::uint32_t>(events.size()), timeout); + } catch (const std::bad_alloc&) { + threw = true; + } + failAllocations = false; + }); + waiter.join(); + Require(threw, "the first wait on a fresh thread fails to allocate its wait state"); + std::atomic<bool> cancelReturned{false}; + std::thread canceller([&] { + Require(sceRudpPollCancel(poll) == 0, "sceRudpPollCancel"); + cancelReturned = true; + }); + const auto start = std::chrono::steady_clock::now(); + while (!cancelReturned && std::chrono::steady_clock::now() - start < std::chrono::seconds(2)) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!cancelReturned) { + std::fprintf(stderr, "failed: a wait that threw left its registration behind and the cancel did not return\n"); + Require(sceRudpPollDestroy(poll) == 0, "sceRudpPollDestroy releases the cancel"); + canceller.join(); + return 1; + } + canceller.join(); + std::array<RudpPollEvent, 4> events{}; + Require(sceRudpPollWait(poll, events.data(), static_cast<std::uint32_t>(events.size()), 0) == cancelled, + "the cancel with no registered waiter is kept for the next wait"); + Require(sceRudpPollWait(poll, events.data(), static_cast<std::uint32_t>(events.size()), 0) == 0, + "only one cancellation is kept"); + } + Require(sceRudpPollDestroy(poll) == 0, "sceRudpPollDestroy"); + Require(sceRudpEnd() == 0, "sceRudpEnd"); + return 0; +} diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index d09fb00499..a709e28c73 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -13,6 +13,6 @@ - [sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5. `(uint16_t mss)` and the checks follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp#L1573-L1584), with the argument in [RPCS3](https://github.com/RPCS3/rpcs3/blob/8b9ee60569753db8406b66d135cce112156172a1/rpcs3/Emu/Cell/Modules/cellRudp.cpp#L149-L153): `RUDP_ERROR_NOT_INITIALIZED` before `sceRudpInit`, `RUDP_ERROR_NOT_ACCEPTABLE` while a context exists, otherwise stored without a range check and kept across `sceRudpEnd`, as Vita3K keeps it in its library state. Nothing reads the stored size: Vita3K [leaves the getter unimplemented](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp#L1080-L1082), so `sceRudpGetMaxSegmentSize` keeps the measured 1410 - [sceRudpGetOption, sceRudpSetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - full PS5 option contracts are unverified; defaults and validation follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). Options 12 and 13 have unknown meanings; last-error option 14 is not maintained. Their setters store all 32 bits per context and return 0 for valid four-byte buffers, preserving acceptance on `main`; their getters throw. Static inspection of PPSA06888 v01.001.008's bundled Unity transport shows its context-set constructor configuring IDs 2, 3, 5 and 6; runtime usage and other paths remain unverified. The plugin wrappers pass four-byte values. No context can connect, so stored settings cannot affect traffic. Closes when: PS5 option contracts are verified and the remaining behavior is implemented. - [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) - console hardware behavior and removal of the last entry during an existing wait remain unverified. A [firmware 13.42 report](https://github.com/boykopovar/AnyPS5/pull/10954#issuecomment-6398457761) executes the actual module under emulation with kernel/libc imports stubbed and condition waits scripted, not on console hardware: an empty poll waits for its remaining timeout, including after spurious wakes, and leaves event output untouched. This agrees with the retained wait behavior here and conflicts with the immediate empty return in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). The argument checks, control operations and 8-byte event layout retain their existing sources; the firmware report has not been independently reproduced here. -- [sceRudpPollCancel](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - a cancel that arrives while an earlier cancel is still draining, and a wait that starts during that drain, are unmeasured: here the drain waits only for the waits that were registered when it started, and a later wait is neither cancelled nor counted. The rest is measured on a retail PS5 (firmware 13.60) through the real `libSceRudp.sprx` by [@HabibUrRehmanBhattii](https://github.com/boykopovar/AnyPS5/pull/11294#issuecomment-6434977474), matching @BunnyFl's [13.42 library run](https://github.com/boykopovar/AnyPS5/pull/10954#issuecomment-6399054346): `int sceRudpPollCancel(int pollId)` returns `RUDP_ERROR_NOT_INITIALIZED` before `sceRudpInit`, `RUDP_ERROR_INVALID_ARGUMENT` for a negative id and `RUDP_ERROR_INVALID_POLL_ID` for an unknown one. With no waiter, one cancellation is kept for that poll however often it is cancelled, and the next `sceRudpPollWait` on it, also with a zero timeout, returns `0x80770014` at once. With waiters, all of them return `0x80770014`, the call returns 0 after they have left, and nothing stays pending. +- [sceRudpPollCancel](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unmeasured: a wait that starts while a cancel is draining. Here the drain waits only for the waits registered when it started; a later wait is neither cancelled nor counted. The rest is measured on a retail PS5 (firmware 13.60) through the real `libSceRudp.sprx` ([@HabibUrRehmanBhattii](https://github.com/boykopovar/AnyPS5/pull/11294#issuecomment-6434977474)), matching @BunnyFl's [13.42 library run](https://github.com/boykopovar/AnyPS5/pull/10954#issuecomment-6399054346): `int sceRudpPollCancel(int pollId)` returns `0x80770001` before `sceRudpInit`, `0x80770004` for a negative id and `0x8077001F` for an unknown one. With no waiter, one cancellation is kept per poll and the next `sceRudpPollWait`, even with a zero timeout, returns `0x80770014` at once. With waiters, all return `0x80770014`, the call returns 0 after they leave, and nothing stays pending. - [sceRudpPollDestroy and sceRudpEnd](../../../core/libs/prx/libSceRudp/Export.cpp) - draining remains incomplete. The [detailed firmware 13.42 report](https://github.com/boykopovar/AnyPS5/pull/10954#issuecomment-6399054346) executes the module with emulated/stubbed imports. In the scripted two-waiter Destroy case both waiters return `0x80770014`; Destroy drains them before returning, then destroys the condition variable. A later wait on that destroyed ID returns `0x8077001F`. End similarly drains its active waiter with `0x80770014`; subsequent Wait/Cancel calls return `0x80770001`. Here Destroy/End return without draining and wake existing waiters with `0x8077001F`/`0x80770001`. Real-kernel wake/timeout races, simultaneous control calls, End/re-init overlap and extreme timeout boundaries remain untested; the report is not a console hardware measurement or an independent reproduction here. - [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo and sceRudpListen](../../../core/libs/prx/libSceRudp/Export.cpp) - remain throwing placeholders. Their names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c); their parameterless declarations solely resolve known imports and do not establish the ABI. From 5bb96d08ad20c8bd508dcf07546d5874580d5183 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:28:34 -0400 Subject: [PATCH 658/702] docs(libkernel): keep the map protection debt entry under the length limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index a3f17b507d..83fccd6b87 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -145,7 +145,7 @@ - [Linux runtime module initialization](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - modules in explicit runtime directories can be mapped for bindings before an explicit guest load runs their entry and init array once with the caller arguments. Finalizers run only after initialization completed. Console dependency-start and finalization ordering are unmeasured. Windows and macOS use their existing lifecycle paths. - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. -- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`); here `sceKernelMapFlexibleMemory`, `sceKernelMapNamedFlexibleMemory`, `sceKernelMapDirectMemory`, `sceKernelMapDirectMemory2` and `sceKernelMapNamedDirectMemory` return `EINVAL` for them as well, before changing any mapping, while `sceKernelMtypeprotect` and the batch map calls throw for them +- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`); here the five flexible and direct map calls do too, before mapping, but `sceKernelMtypeprotect` and the batch map calls throw - [JIT shared memory on macOS](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - the object is an unlinked `mkstemp` file in the temporary directory, as `shm_open` makes its anonymous objects. macOS refuses an executable shared file mapping (`EPERM`), so an executable view is mapped without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view after it ran from another keeps its old translation until the executable view's protection changes; first writes and a single read-write-execute view, as XPSemu PPSA97358 and VLC PPSA85300 map, are seen. `mmap` of a JIT descriptor is not supported, as file-backed guest `mmap` throws on macOS. - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing here is taken from a console except what the kevent measurements entry lists. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters except `EVFILT_AIO`, `EVFILT_LIO`, `EVFILT_SENDFILE` and `EVFILT_PROCDESC` (see the kevent measurements entry), `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry) and every change to an existing timer except `EV_DELETE` and the `EV_ADD` re-adds that the kevent measurements entry describes throw. A `NOTE_NSECONDS` period above one second with a fraction is kept to the nanosecond, where 11.0 converts the fraction with the microsecond macro. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw From 556aa8098de130b3abb50d1e2a9658caadfda5a3 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Wed, 28 Oct 2026 00:36:05 -0500 Subject: [PATCH 659/702] test(shader): keep agc_spirv_optimizer's stored-lane scan inside the module storedLaneFunction took &words[offset + 1] for every instruction, including the final one-word OpFunctionEnd, where offset + 1 equals words.size(). GCC 15 and later define _GLIBCXX_ASSERTIONS in unoptimized builds, so agc_spirv_optimizer, agc_spirv_optimizer_full and agc_spirv_optimizer_none abort in every Debug build with "Assertion '__n < this->size()' failed". Form the operand pointer from words.data(); operands are still read only for instructions that have them. --- core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index b50010c578..1cf79f16d3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -384,7 +384,7 @@ std::optional<std::pair<std::uint32_t, std::uint32_t>> storedLaneFunction(const bool function = false; for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> spv::WordCountShift) { const auto opcode = static_cast<spv::Op>(words[offset] & spv::OpCodeMask); - const auto* operands = &words[offset + 1]; + const auto* operands = words.data() + offset + 1; if (opcode == spv::OpFunction) function = true; if (opcode == spv::OpConstant) values[operands[1]] = {0u, operands[2]}; if (opcode == spv::OpVariable && operands[2] == spv::StorageClassInput) inputs.insert(operands[1]); From dbea3c9c0c1f481f40c598dafa3c4ede2c1f5136 Mon Sep 17 00:00:00 2001 From: B-Howell <116104752+B-Howell@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:41:56 -0400 Subject: [PATCH 660/702] docs(libc): drop the rename dot-destination gap that 308848e1a6 closed rename and sceKernelRename now look the source up before they reject a dot destination, as FreeBSD 11.4's kern_renameat does, so a missing source onto dir/. fails with ENOENT. guest_filesystem covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 3ed6fb9b40..ed2344722f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -128,7 +128,7 @@ - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. - [atan2f](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_atan2f.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_atan2f.c) over this `atanf`, with `tiny` and `pi_lo` read at run time as msun's `volatile` reads do. With two NaN arguments it returns `y`'s, as the titles' compiled `x + y` does. On 200,001,024 argument pairs (random, near-equal exponents, x near ±1, zero y, and every pair of 32 special values), run on a Windows host, it gives the same bits as the `atan2f` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934, and none of those sets `errno`. The other pairs are unverified. -- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `unlink`, `rmdir`, `sceKernelUnlink` and `sceKernelRmdir` follow the same rule. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. +- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `unlink`, `rmdir`, `sceKernelUnlink` and `sceKernelRmdir` follow the same rule. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [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) imports nanf. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws From 1127dacf683173feab08d63d643b69740f74cf01 Mon Sep 17 00:00:00 2001 From: tkcm01 <75453983+tkcm01@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:07:41 -0400 Subject: [PATCH 661/702] fix(libkernel): keep errno across cancellable sleeps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/Time/Time.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 15153fc265..d97b8b752a 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -336,6 +336,7 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint static void CancellableSleep(std::uint64_t nanos) { ThreadCancel::Check(); + const int savedErrno = errno; if (nanos <= SleepWake::MAX_WAKEABLE_NANOS) { SleepWake::Sleep(nanos); } else { @@ -348,6 +349,7 @@ static void CancellableSleep(std::uint64_t nanos) { ThreadCancel::Wait(condition, lock, deadline); } } + errno = savedErrno; ThreadCancel::Check(); } From 835327563cfea13d030ccf1635a31fe4729db17e Mon Sep 17 00:00:00 2001 From: tkcm01 <75453983+tkcm01@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:07:48 -0400 Subject: [PATCH 662/702] fix(json2): accept a number that underflows to zero on libc++ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceJson2/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 477b3d020f..18ddc3103b 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -464,7 +464,7 @@ private: std::istringstream stream(text); stream.imbue(std::locale::classic()); stream >> real; - converted = !stream.fail() && stream.eof(); + converted = stream.eof() && (!stream.fail() || real == 0); } if (!converted || !std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified"); out.type = TypeReal; From e26d34d624d2fa46724f9cd58b70f9a15b1d6937 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Wed, 28 Oct 2026 00:51:32 -0500 Subject: [PATCH 663/702] docs(build): say APS5_AGC_CREATE_LOG has no effect The option's only use, the success log in sceAgcCreateShader, has been commented out since 3da76b9b33, so OFF changes nothing and ON logs nothing. --- docs/dev/BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 1154ea812c..bc124b0d90 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -119,7 +119,7 @@ Project switches accept `ON` or `OFF`: | `-DANYPS5_USE_SYSTEM_DEPS=ON` | `OFF` | Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the submodules. | | `-DAPS5_ENABLE_TIMING_LOG=ON` | `OFF` | Compile frame timing logging. | | `-DAPS5_ENABLE_TRACY=ON` | `OFF` | Build the [Tracy](#tracy) profiler client into the agc driver. | -| `-DAPS5_AGC_CREATE_LOG=OFF` | `ON` | Disable successful `sceAgcCreateShader` logging. | +| `-DAPS5_AGC_CREATE_LOG=OFF` | `ON` | No effect: `sceAgcCreateShader` logs nothing on success. | | `-DAGC_BUILD_VISUAL_TEST=ON` | `OFF` | Build the standalone AGC SPIR-V visual test. | GCC and Clang compile libc with `-ffp-contract=off` whatever the target CPU. Toolchains that default to x86-64-v3, such as GCC 15 on Ubuntu 26.04, would otherwise fuse `a * b + c` into FMA instructions, which changes the last bit of math results that must match the console. From 70d2036621a9056b2d7eda271dbcd17df09b8d3d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:04:42 +0000 Subject: [PATCH 664/702] fix(hw-oracle): report a deeply nested JSON report as invalid read_report passes the file to json.loads, and a report nested a few hundred thousand levels deep raised RecursionError, which main does not catch. The command ended in a traceback with exit code 1, the code the tool documents as 'different outputs', so a corrupt file looked like a hardware difference. The error is now a ValueError naming the file, and main reports it with exit code 2 like every other invalid report. --- tools/hw-oracle/compare_results.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/hw-oracle/compare_results.py b/tools/hw-oracle/compare_results.py index f049a8983c..9171bfe5d5 100644 --- a/tools/hw-oracle/compare_results.py +++ b/tools/hw-oracle/compare_results.py @@ -14,7 +14,10 @@ def integer(value, minimum, maximum): def read_report(path): - report = json.loads(path.read_text(encoding="utf-8-sig")) + try: + report = json.loads(path.read_text(encoding="utf-8-sig")) + except RecursionError as error: + raise ValueError(f"{path}: JSON nested too deeply") from error if not isinstance(report, dict): raise ValueError(f"{path}: expected a JSON object") required = {"schema_version", "assembly_target", "rows", *SETTINGS} From 68a017c9fea40cb6396c9e9523b2611fe902c4c7 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:04:43 +0000 Subject: [PATCH 665/702] fix(tools): report a deeply nested registry as an audit error in import_audit read_registry and read_needed_registry catch OSError, UnicodeDecodeError and JSONDecodeError, but a registry nested a few hundred thousand levels deep raises RecursionError. main returns 2 for an AuditError and 1 for missing imports, so the traceback ended with exit code 1 and a corrupt registry looked like a failed import audit. RecursionError is now caught with the other read errors and reported as an AuditError, so the exit code is 2. --- tools/import_audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 93aa0a865f..18a688f29a 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -656,7 +656,7 @@ def source_stub_classes(source): def read_registry(path): try: entries = json.loads(Path(path).read_text(encoding="utf-8-sig")) - except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: + except (OSError, UnicodeDecodeError, json.JSONDecodeError, RecursionError) as error: raise AuditError(f"{path}: cannot read registry: {error}") from error if not isinstance(entries, list): raise AuditError(f"{path}: a registry is a JSON list, got {type(entries).__name__}") @@ -681,7 +681,7 @@ def needed_registry_path(path): def read_needed_registry(path): try: libraries = json.loads(path.read_text(encoding="utf-8-sig")) - except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: + except (OSError, UnicodeDecodeError, json.JSONDecodeError, RecursionError) as error: raise AuditError(f"{path}: cannot read needed libraries: {error}") from error if not isinstance(libraries, list) or any(not isinstance(library, str) or not library for library in libraries): raise AuditError(f"{path}: needed libraries must be a JSON list of nonempty strings") From ffe35060940a05904e7e6e2e697d3f5ab7e93769 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:04:43 +0000 Subject: [PATCH 666/702] test(web): run archive-reserved.test.mjs with the other archive tests tools/test_web.py launched archive-root, archive-windows and archive.test but not archive-reserved.test.mjs, which covers the superscript Windows device names (COM1 with a superscript digit, LPT2 and so on) added with 3397e873bc. No workflow, script or CTest entry referenced it, so a regression in validPath for those names went unnoticed. It runs next to the other archive tests now. --- tools/test_web.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/test_web.py b/tools/test_web.py index c3dc394494..ca0ccf9fba 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -44,6 +44,7 @@ def main(argv=None): environment = dict(os.environ, ANYPS5_TEST_ZIP_URL=url, ANYPS5_TEST_ZIP_PATH=library.as_uri()) subprocess.run(["node", str(ROOT / "demo/web/tests/archive-root.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) subprocess.run(["node", str(ROOT / "demo/web/tests/archive-windows.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive-reserved.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) except (subprocess.CalledProcessError, urllib.error.URLError, OSError) as error: parser.error(str(error)) From 8c045ebe3709dd5b16dc306b05a40928a72a8606 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:04:59 +0000 Subject: [PATCH 667/702] test(hw-oracle): compare a report nested far deeper than the JSON parser allows The command has to exit with 2 and say the file is nested too deeply, without a traceback, instead of exiting with the code that means different outputs. --- tools/hw-oracle/test_compare_results.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/hw-oracle/test_compare_results.py b/tools/hw-oracle/test_compare_results.py index d33880b06e..03c1816db5 100644 --- a/tools/hw-oracle/test_compare_results.py +++ b/tools/hw-oracle/test_compare_results.py @@ -33,6 +33,15 @@ class CompareResultsTests(unittest.TestCase): self.assertEqual(result.returncode, int(different), result.stdout + result.stderr) self.assertEqual(result.stderr, "") + def test_deeply_nested_json_is_an_invalid_report_not_a_difference(self): + self.before.write_text(json.dumps(self.report), encoding="utf-8") + self.after.write_text("[" * 200000, encoding="utf-8") + result = subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("nested too deeply", result.stderr) + self.assertNotIn("Traceback", result.stderr) + def test_bom_does_not_hide_invalid_json_or_invalid_reports(self): self.before.write_text(json.dumps(self.report), encoding="utf-8") for payload in ("{", "[]", '{"schema_version": 2}'): From 70cd9c91c2afdb3348e644b83772346c7058acfc Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:04:59 +0000 Subject: [PATCH 668/702] test(tools): audit imports from registries nested far deeper than the JSON parser allows A registry and a needed-libraries file of 200000 opening brackets have to end in an audit error with exit code 2 and the read-error message, not a traceback. --- tools/tests/test_import_audit.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 1cd1d255bf..54ba8be91c 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -242,6 +242,18 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(result.stdout, b"") + def test_deeply_nested_registries_are_audit_errors(self): + registry = self.work.root / "game.registry.json" + registry.write_text("[" * 200000, encoding="utf-8") + code, out, err = self.work.run(registry) + self.assertEqual(code, 2, err) + self.assertIn("cannot read registry", err) + registry.write_text("[]", encoding="utf-8") + registry.with_name("game.needed.json").write_text("[" * 200000, encoding="utf-8") + code, out, err = self.work.run(registry) + self.assertEqual(code, 2, err) + self.assertIn("cannot read needed libraries", err) + def test_windows_guest_dependencies_use_host_prx_suffixes(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) From 0de89c89ebf83101de6b3192a73329af54c8c204 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Wed, 28 Oct 2026 00:47:58 -0500 Subject: [PATCH 669/702] docs(debt): narrow libSceHmd's protected-body notes to the throwing queries The work-memory entry said the bodies of all its queries remain unimplemented, but sceHmdGetDistortionWorkMemoryAlign and Size return 0x400 and 0x20000 since db2575839 (#8307), and their sceHmdDistortionGetWorkMemory* aliases since fa916ae7bc (#8256). Only capture start, separate-mode open, crash-report cancellation and the mirroring aspect debug setter still throw. The device and sensor query entry said its protected bodies are unchanged, but sceHmdGetFieldOfView, sceHmdGetInertialSensorData and sceHmdGetAssyError answer without a headset since fe6aba89a8 and a569d075ec (#8256), before 7b97d7552c (#9137) wrote the entry. Only the internal device-information, distortion-parameter, status-report and DFU-status queries still throw. --- docs/dev/debt/libSceHmd.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index cf169b2329..01241796f5 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -13,11 +13,11 @@ - [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) returns a constant error without consuming arguments in the supplied module; unused argument types remain unresolved. - [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) takes an unused handle, a 32-bit mask output and a nine-byte LED output. Output sizes match the paired setter; mask bits and LED payload semantics remain unresolved. The protected body is unchanged. -[Device, field-of-view, assembly-error, distortion, sensor and DFU query signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered from native register use. Field of view and assembly errors each output four floats; device information occupies 64 bytes and distortion output spans 76 bytes. The remaining device/sensor/report field semantics and DFU status bit meanings are unresolved. Assembly confirms integer error returns where decompilation incorrectly inferred void. Protected bodies are unchanged. +[Device, field-of-view, assembly-error, distortion, sensor and DFU query signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered from native register use. Field of view and assembly errors each output four floats; device information occupies 64 bytes and distortion output spans 76 bytes. The remaining device/sensor/report field semantics and DFU status bit meanings are unresolved. Assembly confirms integer error returns where decompilation incorrectly inferred void. The internal device-information, distortion-parameter, status-report and DFU-status queries throw. [Reprojection initialization and display-buffer signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered: initialization takes a parameter pointer, 32-bit mode and null reserved pointer; display-buffer setup takes a VideoOut handle, two buffer indices and reserved pointer, while adding a buffer takes an index and reserved pointer. Parameter fields remain incomplete. Protected bodies are unchanged. -[Work-memory queries, capture start, separate-mode open and crash-report cancellation](../../../core/libs/prx/libSceHmd/Export.cpp) consume no arguments in the supplied native implementation. The mirroring aspect debug setter takes five 16-bit values followed by a byte mode; the individual scalar meanings remain unresolved. These argument lists are confirmed; protected bodies remain unimplemented. +[Work-memory queries, capture start, separate-mode open and crash-report cancellation](../../../core/libs/prx/libSceHmd/Export.cpp) consume no arguments in the supplied native implementation. The mirroring aspect debug setter takes five 16-bit values followed by a byte mode; the individual scalar meanings remain unresolved. These argument lists are confirmed; capture start, separate-mode open, crash-report cancellation and the mirroring aspect debug setter throw. [Reprojection start signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered, including the five arguments of `StartMultilayer2` hidden by an incomplete decompilation. Standard and wide-near starts take frame parameters, pose, 64-bit flip argument and reserved pointer; overlay variants add an overlay descriptor. The 2D variant takes parameters, flip argument and reserved pointer. Multilayer takes a layer array/count, submission parameters, pose, flip argument and reserved pointer. Frame, pose, overlay and submission field semantics remain incomplete. Protected bodies are unchanged. From e31fad5ae866feeb6e5bff4a3f1aa1b19584c2e3 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:30:37 -0400 Subject: [PATCH 670/702] test(libSceRudp): link the wait-exception test against libc for its exception personality Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/CMakeLists.txt | 4 +++- core/libs/tests/RudpPollWaitException.cpp | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 95c9b0c70d..8ff98f9ba8 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -816,9 +816,11 @@ if(BUILD_TESTING) add_test_executable(rudp_poll_wait_exception_tests tests/RudpPollWaitException.cpp prx/libSceRudp/Export.cpp prx/libkernel/Time/TimedWait.cpp) target_include_directories(rudp_poll_wait_exception_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(rudp_poll_wait_exception_tests PRIVATE libc) configure_windows_unwind(rudp_poll_wait_exception_tests) add_test(NAME rudp_poll_wait_exception COMMAND rudp_poll_wait_exception_tests) - set_tests_properties(rudp_poll_wait_exception PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77) + set_tests_properties(rudp_poll_wait_exception PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) diff --git a/core/libs/tests/RudpPollWaitException.cpp b/core/libs/tests/RudpPollWaitException.cpp index 65b72077e2..f9cf2f4d4f 100644 --- a/core/libs/tests/RudpPollWaitException.cpp +++ b/core/libs/tests/RudpPollWaitException.cpp @@ -10,7 +10,6 @@ #include <cstdlib> #include <limits> #include <new> -#include <stdexcept> #include <thread> namespace { @@ -51,10 +50,6 @@ void operator delete[](void* memory, std::size_t) noexcept { std::free(memory); } -extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* name) { - throw std::runtime_error(name); -} - int main() { if (TimedWait::Coarse()) return 77; constexpr int cancelled = static_cast<int>(0x80770014u); From b5c728e6bf4288c68dba58d60de016566b4f9f81 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Wed, 28 Oct 2026 01:43:47 -0500 Subject: [PATCH 671/702] docs(debt): drop libSceMove's stale protected-body note for open and close sceMoveClose returns INVALID_HANDLE for a handle that is not open since 808d67e715 (#8252), and sceMoveOpen returns ALREADY_OPENED for an open slot since c478789949 (#8252) and throws only for users other than the initial user since 95ea06e80b (#8125). The sentence came from 8a1bd363ef (#9137), which left both bodies alone; the rest of the entry already describes them. --- docs/dev/debt/libSceMove.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceMove.md b/docs/dev/debt/libSceMove.md index 3b9e712351..1d8f953336 100644 --- a/docs/dev/debt/libSceMove.md +++ b/docs/dev/debt/libSceMove.md @@ -3,5 +3,5 @@ ### Functional - [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp): the host implements only disconnected controllers. The native module confirms initialization errors, disconnected return 1 without output writes, invalid-handle error 0x80EE0004, and initialization then argument then handle validation for queries. Reset-light-sphere still omits the device ioctl and its failure mapping. -- [sceMoveOpen and sceMoveClose](../../../core/libs/prx/libSceMove/Export.cpp): protected bodies remain unchanged. Native opening resolves a user/device through the input service, rejects duplicate device handles with 0x80EE0007, limits storage to eight slots with 0x80EE0005, and obtains handles from the device ioctl. The host generates monotonically increasing handles, accepts arbitrary type/index values and returns 0x80EE0007 for duplicate slots and throws for other users. Closing an unknown handle returns the confirmed 0x80EE0004. +- [sceMoveOpen and sceMoveClose](../../../core/libs/prx/libSceMove/Export.cpp): native opening resolves a user/device through the input service, rejects duplicate device handles with 0x80EE0007, limits storage to eight slots with 0x80EE0005, and obtains handles from the device ioctl. The host generates monotonically increasing handles, accepts arbitrary type/index values and returns 0x80EE0007 for duplicate slots and throws for other users. Closing an unknown handle returns the confirmed 0x80EE0004. - [sceMoveReadStateRecent](../../../core/libs/prx/libSceMove/Export.cpp): a null data array is accepted for SDK versions through 0x03ffffff and rejected from 0x04000000 on, as the native module does; the count pointer and a nonnegative timestamp are required for every version. Connected state records and device-service failures remain unimplemented. From 6b578858260ab46ed3a5aad48410a0946d2a0dce Mon Sep 17 00:00:00 2001 From: Henryisdjfunkyho <197676650+Henryisdjfunkyho@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:49:14 -0700 Subject: [PATCH 672/702] fix(tools): clarify existing NID name status --- tools/nid_names.py | 6 +++--- tools/tests/test_nid_names.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index 920876f1b3..767f55d13d 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -252,7 +252,7 @@ def main(argv=None): status = "MISMATCH" failures += 1 elif suggestion in real_names: - status = "already-implemented" + status = "name-in-use" else: status = "rename-ready" rows.append({"nid": nid, "stub": unknowns[nid], @@ -265,9 +265,9 @@ def main(argv=None): for row in rows: print("%-12s %-40s %-12s %s" % ( row["nid"], row["stub"][:40], row["status"], row["suggestion"])) - print("%d unknowns, %d suggestions (%d already implemented)" % ( + print("%d unknowns, %d suggestions (%d names already in use)" % ( len(rows), sum(1 for r in rows if r["suggestion"]), - sum(1 for r in rows if r["status"] == "already-implemented"))) + sum(1 for r in rows if r["status"] == "name-in-use"))) return 1 if failures else 0 diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 59e76c64ca..ae46f50756 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -167,7 +167,7 @@ class DatabaseCacheTests(unittest.TestCase): if json_output: self.assertEqual(json.loads(result.stdout), {"db_entries": 0, "rows": []}) else: - self.assertIn("0 unknowns, 0 suggestions (0 already implemented)", result.stdout) + self.assertIn("0 unknowns, 0 suggestions (0 names already in use)", result.stdout) def test_environment_variable_selects_the_database_and_help_names_it(self): script = str(Path(nid_names.__file__)) @@ -435,6 +435,34 @@ class SourceCollectionTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", errors="replace")) self.assertIn("core/libs/prx/libSceExample/\u8cc7\u6599.cpp", result.stdout.decode("utf-8")) + def test_named_stub_and_implementation_are_reported_as_names_in_use(self): + stub_name, implementation_name = "scePadPending", "scePadImplemented" + stub_nid = nid_names.compute_nid(stub_name) + implementation_nid = nid_names.compute_nid(implementation_name) + self.write_source( + f'APS5_EXPORT("{stub_nid}", exampleUnknown00);\n' + f'APS5_EXPORT("{implementation_nid}", exampleUnknown01);\n' + 'int APS5_VABI scePadPending() { NotImplemented_nid_no_patch(__func__); return 0; }\n' + 'int APS5_VABI scePadImplemented() { return 1; }\n') + database = self.root / "aerolib.csv" + database.write_text(f"{stub_nid} {stub_name}\n{implementation_nid} {implementation_name}\n", encoding="utf-8") + + text_result = self.cli("--db", database) + self.assertEqual(text_result.returncode, 0, text_result.stderr.decode("utf-8", errors="replace")) + text = text_result.stdout.decode("utf-8") + self.assertIn(f"{stub_nid} ", text) + self.assertIn(f"{implementation_nid} ", text) + self.assertIn("name-in-use scePadPending", text) + self.assertIn("name-in-use scePadImplemented", text) + self.assertEqual(text.count("name-in-use"), 2) + self.assertIn("2 names already in use", text) + + json_result = self.cli("--db", database, "--json") + self.assertEqual(json_result.returncode, 0, json_result.stderr.decode("utf-8", errors="replace")) + rows = json.loads(json_result.stdout.decode("utf-8"))["rows"] + self.assertEqual({row["nid"]: row["status"] for row in rows}, + {stub_nid: "name-in-use", implementation_nid: "name-in-use"}) + def test_error_preserves_unicode_paths_with_legacy_stderr(self): database = self.root / "\u8cc7\u6599" database.mkdir() From 762a280ee33e5f30cee5f580c8698d28106027d2 Mon Sep 17 00:00:00 2001 From: Gruzzberry <angus@hutchisons.org> Date: Sun, 25 Oct 2026 13:58:59 +0000 Subject: [PATCH 673/702] feat(libkernel_sys): implement kenv Follow FreeBSD 11.4 sys_kenv: KENV_GET, KENV_SET, KENV_UNSET and KENV_DUMP over an in-memory kernel environment of at most 512 variables, with the 128-byte name and value limits, ENOENT, EINVAL, ENAMETOOLONG and EFAULT. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- .../prx/libkernel_sys/Export.Discovery.cpp | 5 - core/libs/prx/libkernel_sys/Export.cpp | 86 +++++++++++++++++ core/libs/prx/libkernel_sys/include/Api.hpp | 2 +- core/libs/tests/GuestKernelSys.cpp | 95 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 5 files changed, 183 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 2929b25044..0a8f697d6e 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -110,11 +110,6 @@ int APS5_VABI is_in_sandbox_nid_postfix(void) { return 0; } -int APS5_VABI kenv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI kldload_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 88b46e2eba..55e4a22dff 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -9,10 +9,12 @@ #include <cstdlib> #include <filesystem> #include <limits> +#include <mutex> #include <stdexcept> #include <string> #include <string_view> #include <system_error> +#include <vector> #include <sys/stat.h> #include <ctime> #ifndef _WIN32 @@ -129,6 +131,35 @@ std::int64_t Fail(int error) { return -1; } +constexpr int KenvGet = 0; +constexpr int KenvSet = 1; +constexpr int KenvUnset = 2; +constexpr int KenvDump = 3; +constexpr std::size_t KenvNameLength = 128; +constexpr std::size_t KenvValueLength = 128; +constexpr std::size_t KenvEntries = 512; + +struct KernelEnvironment { + std::mutex mutex; + std::vector<std::string> entries; +}; + +KernelEnvironment& KernelEnv() { + static auto* environment = new KernelEnvironment; + return *environment; +} + +std::vector<std::string>::iterator FindKernelEnv(std::vector<std::string>& entries, const std::string& name) { + return std::find_if(entries.begin(), entries.end(), [&](const std::string& entry) { + return entry.size() > name.size() && entry.compare(0, name.size(), name) == 0 && entry[name.size()] == '='; + }); +} + +int CopyKernelEnvString(const char* source, std::size_t limit, std::string& destination) { + if (const int error = CopyGuestPath_nid_no_patch(source, destination)) return error; + return destination.size() + 1 > limit ? GuestEnametoolong : 0; +} + std::int64_t PathConfigurationValue(int name) { switch (name) { case PcLinkMax: return 32767; @@ -375,6 +406,61 @@ int APS5_VABI fstatfs_nid_postfix(int d, KernelStatfs* buf) { return _fstatfs_nid_postfix(d, buf); } +int APS5_VABI kenv_nid_postfix(int what, const char* name, char* value, int len) { + auto& environment = KernelEnv(); + if (what == KenvDump) { + std::size_t capacity = len > 0 ? static_cast<std::size_t>(len) : 0; + if (capacity > KenvEntries * (KenvNameLength + KenvValueLength + 2)) capacity = KenvEntries * (KenvNameLength + KenvValueLength + 2); + const bool copy = len > 0 && value != nullptr; + std::string buffer; + std::size_t needed = 0; + { + const std::lock_guard lock(environment.mutex); + for (const auto& entry : environment.entries) { + const std::size_t size = entry.size() + 1; + needed += size; + if (copy) { + const std::size_t taken = std::min(size, capacity - buffer.size()); + buffer.append(entry.c_str(), taken); + } + } + } + if (copy && !GuestCopyOut_nid_no_patch(value, buffer.data(), buffer.size())) return static_cast<int>(Fail(GuestEfault)); + return buffer.size() == needed ? 0 : static_cast<int>(needed); + } + std::string key; + if (const int error = CopyKernelEnvString(name, KenvNameLength + 1, key)) return static_cast<int>(Fail(error)); + const std::lock_guard lock(environment.mutex); + auto& entries = environment.entries; + switch (what) { + case KenvGet: { + const auto found = FindKernelEnv(entries, key); + if (found == entries.end()) return static_cast<int>(Fail(GuestEnoent)); + const std::size_t size = found->size() - key.size(); + const std::size_t limit = static_cast<std::size_t>(static_cast<std::int64_t>(len)); + const std::size_t taken = std::min(size, limit); + if (!GuestCopyOut_nid_no_patch(value, found->c_str() + key.size() + 1, taken)) return static_cast<int>(Fail(GuestEfault)); + return static_cast<int>(taken); + } + case KenvSet: { + if (len == 0) return static_cast<int>(Fail(GuestEinval)); + std::string text; + if (const int error = CopyKernelEnvString(value, std::min<std::size_t>(len, KenvValueLength + 1), text)) return static_cast<int>(Fail(error)); + const auto found = FindKernelEnv(entries, key); + if (found != entries.end()) *found = key + "=" + text; + else if (entries.size() < KenvEntries) entries.push_back(key + "=" + text); + return 0; + } + case KenvUnset: { + const auto found = FindKernelEnv(entries, key); + if (found == entries.end()) return static_cast<int>(Fail(GuestEnoent)); + entries.erase(found); + return 0; + } + default: return static_cast<int>(Fail(GuestEinval)); + } +} + int GetModuleInfoExByHandle_nid_no_patch(KernelModule handle, ModuleInfoEx* info); int APS5_VABI sceKernelGetModuleInfoInternal(KernelModule handle, ModuleInfoEx* info) { diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index f98537d788..5f24dd4e34 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -109,7 +109,7 @@ int APS5_VABI get_vm_map_timestamp_nid_postfix(void); int APS5_VABI getfsstat_nid_postfix(void); int APS5_VABI ipmimgr_call_nid_postfix(void); int APS5_VABI is_in_sandbox_nid_postfix(void); -int APS5_VABI kenv_nid_postfix(void); +int APS5_VABI kenv_nid_postfix(int what, const char* name, char* value, int len); int APS5_VABI kldfind_nid_postfix(const char* file); int APS5_VABI kldfirstmod_nid_postfix(int fileid); int APS5_VABI kldload_nid_postfix(void); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 32a2da762b..012c3ed514 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -767,6 +767,100 @@ void CheckPipePathconf(F fn) { Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); } +int Kenv(int what, const char* name, const char* value, int len) { + return kenv_nid_postfix(what, name, const_cast<char*>(value), len); +} + +void CheckKernelEnvironment() { + constexpr int get = 0, set = 1, unset = 2, dump = 3; + char buffer[300]; + std::memset(buffer, 0x5a, sizeof(buffer)); + Require(Kenv(dump, nullptr, nullptr, 0) == 0); + Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(Kenv(unset, "anyps5.alpha", nullptr, 0) == -1 && Error() == 2); + Require(Kenv(4, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(Kenv(-1, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(Kenv(set, "anyps5.alpha", "one", 0) == -1 && Error() == 22); + Require(Kenv(set, "anyps5.alpha", "one", 3) == -1 && Error() == 63); + Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(Kenv(set, "anyps5.negative", "one", -1) == 0); + Require(Kenv(get, "anyps5.negative", buffer, sizeof(buffer)) == 4 && std::strcmp(buffer, "one") == 0); + Require(Kenv(set, "anyps5.negative", std::string(128, 'v').c_str(), -1) == 0); + Require(Kenv(set, "anyps5.negative", std::string(129, 'v').c_str(), -1) == -1 && Error() == 63); + Require(Kenv(unset, "anyps5.negative", nullptr, 0) == 0); + + Require(Kenv(set, "anyps5.alpha", "one", 4) == 0); + Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == 4 && std::strcmp(buffer, "one") == 0); + Require(Kenv(get, "anyps5", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(Kenv(get, "anyps5.alph", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(Kenv(get, "anyps5.alphaa", buffer, sizeof(buffer)) == -1 && Error() == 2); + std::memset(buffer, 0x5a, sizeof(buffer)); + Require(Kenv(get, "anyps5.alpha", buffer, 2) == 2 && buffer[0] == 'o' && buffer[1] == 'n' && buffer[2] == 0x5a); + Require(Kenv(get, "anyps5.alpha", buffer, 0) == 0 && buffer[2] == 0x5a); + Require(Kenv(get, "anyps5.alpha", nullptr, 0) == 0); + Require(Kenv(get, "anyps5.alpha", nullptr, 4) == -1 && Error() == 14); + Require(Kenv(get, "anyps5.alpha", reinterpret_cast<char*>(0x10), 4) == -1 && Error() == 14); + Require(Kenv(set, "anyps5.alpha", "", 1) == 0); + Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == 1 && buffer[0] == '\0'); + Require(Kenv(set, "anyps5.alpha", "one", 129) == 0); + Require(Kenv(set, "anyps5.beta", "two", 1000) == 0); + Require(Kenv(set, "anyps5.alpha", "uno", 4) == 0); + Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == 4 && std::strcmp(buffer, "uno") == 0); + + constexpr char expected[] = "anyps5.alpha=uno\0anyps5.beta=two"; + constexpr int needed = sizeof(expected); + Require(Kenv(dump, nullptr, nullptr, 0) == needed); + Require(Kenv(dump, nullptr, buffer, 0) == needed); + Require(Kenv(dump, nullptr, nullptr, sizeof(buffer)) == needed); + std::memset(buffer, 0x5a, sizeof(buffer)); + Require(Kenv(dump, nullptr, buffer, sizeof(buffer)) == 0 && std::memcmp(buffer, expected, needed) == 0 && buffer[needed] == 0x5a); + Require(Kenv(dump, nullptr, buffer, needed) == 0); + std::memset(buffer, 0x5a, sizeof(buffer)); + Require(Kenv(dump, nullptr, buffer, 20) == needed && std::memcmp(buffer, expected, 20) == 0 && buffer[20] == 0x5a); + Require(Kenv(dump, nullptr, reinterpret_cast<char*>(0x10), sizeof(buffer)) == -1 && Error() == 14); + Require(Kenv(dump, reinterpret_cast<const char*>(0x10), buffer, sizeof(buffer)) == 0); + + Require(Kenv(get, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); + Require(Kenv(get, reinterpret_cast<const char*>(0x10), buffer, sizeof(buffer)) == -1 && Error() == 14); + Require(Kenv(4, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); + Require(Kenv(set, "anyps5.gamma", nullptr, 4) == -1 && Error() == 14); + Require(Kenv(set, "anyps5.gamma", reinterpret_cast<const char*>(0x10), 4) == -1 && Error() == 14); + Require(Kenv(get, "anyps5.gamma", buffer, sizeof(buffer)) == -1 && Error() == 2); + + const std::string longest(128, 'n'); + const std::string longestValue(128, 'v'); + Require(Kenv(set, (longest + "x").c_str(), "v", 2) == -1 && Error() == 63); + Require(Kenv(get, (longest + "x").c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); + Require(Kenv(set, longest.c_str(), longestValue.c_str(), 129) == 0); + Require(Kenv(get, longest.c_str(), buffer, sizeof(buffer)) == 129 && std::strcmp(buffer, longestValue.c_str()) == 0); + Require(Kenv(set, "anyps5.gamma", (longestValue + "v").c_str(), 1000) == -1 && Error() == 63); + Require(Kenv(set, "anyps5.gamma", longestValue.c_str(), 128) == -1 && Error() == 63); + Require(Kenv(unset, longest.c_str(), nullptr, 0) == 0); + Require(Kenv(get, longest.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); + + Require(Kenv(set, "anyps5.eq=name", "tail", 5) == 0); + Require(Kenv(get, "anyps5.eq", buffer, sizeof(buffer)) == 10 && std::strcmp(buffer, "name=tail") == 0); + Require(Kenv(unset, "anyps5.eq", nullptr, 0) == 0); + Require(Kenv(unset, "anyps5.eq", nullptr, 0) == -1 && Error() == 2); + Require(Kenv(unset, "anyps5.alpha", nullptr, 0) == 0); + Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(Kenv(dump, nullptr, nullptr, 0) == static_cast<int>(sizeof("anyps5.beta=two"))); + Require(Kenv(unset, "anyps5.beta", nullptr, 0) == 0); + Require(Kenv(dump, nullptr, nullptr, 0) == 0); + + for (int i = 0; i < 512; ++i) Require(Kenv(set, ("anyps5.fill" + std::to_string(i)).c_str(), "x", 2) == 0); + Require(Kenv(set, "anyps5.overflow", "x", 2) == 0); + Require(Kenv(get, "anyps5.overflow", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(Kenv(set, "anyps5.fill0", "y", 2) == 0); + Require(Kenv(get, "anyps5.fill0", buffer, sizeof(buffer)) == 2 && buffer[0] == 'y'); + Require(Kenv(unset, "anyps5.fill511", nullptr, 0) == 0); + Require(Kenv(set, "anyps5.overflow", "x", 2) == 0); + Require(Kenv(get, "anyps5.overflow", buffer, sizeof(buffer)) == 2); + for (int i = 0; i < 511; ++i) Require(Kenv(unset, ("anyps5.fill" + std::to_string(i)).c_str(), nullptr, 0) == 0); + Require(Kenv(unset, "anyps5.overflow", nullptr, 0) == 0); + Require(Kenv(dump, nullptr, nullptr, 0) == 0); +} + int main() { #ifdef _WIN32 auto hostDirectorySpelling = std::filesystem::current_path().native(); @@ -788,6 +882,7 @@ int main() { CheckAmmWaitConcurrentSubmits(); CheckSchedSetscheduler(); CheckSigreturn(); + CheckKernelEnvironment(); const auto root = std::filesystem::path("anyps5-kernel-sys-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index f1838960dd..7787f9adde 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -11,6 +11,7 @@ ### Functional +- [kenv](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_kenv`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_environment.c#L87-L204) over an in-memory table that starts empty, since the console's boot environment is unmeasured, so a variable the system sets at boot reads as `ENOENT`. `KENV_SET` and `KENV_UNSET` always pass the privilege check, as the guest is uid 0. A name or value is read with the guest path copy, which reads up to 1024 bytes, so an unterminated string followed by an unreadable page fails with `EFAULT` where FreeBSD's bounded `copyinstr` gives `ENAMETOOLONG`. A 513th variable is dropped silently, as in FreeBSD. Console behaviour is unverified - [sched_rr_get_interval](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) accepts pid 0 or the host-backed guest pid and reports the FreeBSD 11 ULE default time slice for the usual AMD64 clock rates (94 ms); other pids return `ESRCH`, and a null interval returns `EFAULT`. AnyPS5 does not run a guest ULE scheduler or track a per-process configured quantum, so this reports the modeled default and does not reflect runtime scheduling. Console-specific clock rates and quantum configuration are unverified - [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, `EINVAL` for a pipe or socket, as FreeBSD `getvnode` does for a descriptor without a vnode, and `ENOTDIR` for any other descriptor that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat, _openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) pass `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. They return `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NONBLOCK` is ignored. PPSA99003 opens directory entries through `openat`. Console behaviour is unverified From fe7a71c1ea0bbd6fe1a6332d6f6c8abe38c89d2b Mon Sep 17 00:00:00 2001 From: B-Howell <116104752+B-Howell@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:01:08 -0400 Subject: [PATCH 674/702] fix(libkernel): classify the rmdir path with lstat so links give ENOTDIR on Windows sceKernelRmdir decided whether the last component was a symbolic link with std::filesystem::is_symlink, which follows the link with WinLibs libstdc++ on Windows. A dangling or self-referencing file link then looked missing on Windows 10 (ENOENT instead of ENOTDIR), and a directory link to an empty directory looked like that directory, so rmdir removed the link and returned 0. The last component is now classified with FillLinkStat, the same lstat path lstat_nid_postfix uses, matching FreeBSD 11 kern_rmdirat, which returns ENOTDIR for any non-directory vnode found without following the last link. Fixes #12079 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/File/src/Stdio.cpp | 6 ++++-- core/libs/tests/GuestFilesystem.cpp | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index fbecc93658..0120376c9b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1645,8 +1645,10 @@ int APS5_VABI sceKernelRmdir(const char* path) { if (const int dot = RenameDotError_nid_no_patch(path)) return SceKernelError(dot); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); + FileStat linkStatus{}; + if (native.has_filename() && File::FillLinkStat(native, &linkStatus) && (linkStatus.st_mode & 0170000) == 0120000) return SceErrorFromErrno(GUEST_ENOTDIR); std::error_code error; - if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GUEST_ENOTDIR); + const auto status = std::filesystem::status(native, error); if (const int failure = PathLookupFailure(error)) return failure; #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.c_str()); @@ -1655,7 +1657,7 @@ int APS5_VABI sceKernelRmdir(const char* path) { if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "rmdir")) return SceErrorFromErrno(GUEST_ENOTDIR); } #endif - if (!std::filesystem::is_directory(native, error)) { + if (!std::filesystem::is_directory(status)) { #ifdef _WIN32 if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(native).parent_path())) return SceKernelError(GUEST_ELOOP); #endif diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index e303ad72f9..c854d40b34 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1061,6 +1061,11 @@ int main() { std::filesystem::create_symlink("missing.txt", dangling); const auto statLoop = root / "stat-loop"; std::filesystem::create_symlink("stat-loop", statLoop); + for (const auto* linkPath : {&dangling, &statLoop}) { + Require(sceKernelRmdir(linkPath->string().c_str()) == static_cast<int>(0x80020014u)); + Require(rmdir_nid_postfix(linkPath->string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(lstat_nid_postfix(linkPath->string().c_str(), &linkStatus) == 0 && (linkStatus.st_mode & 0170000) == 0120000); + } errno = 0; Require(!File::TryFillFileStat(statLoop, &status) && errno == ELOOP); Require(sceKernelStat(statLoop.generic_string().c_str(), &status) == SCE_KERNEL_ERROR_ELOOP); @@ -1123,6 +1128,19 @@ int main() { Require(sceKernelRmdir(belowStatLoop.c_str()) == static_cast<int>(0x8002003eu)); Require(rmdir_nid_postfix(belowStatLoop.c_str()) == -1 && *__error_nid_postfix() == 62); Require(sceKernelRmdir(statLoop.string().c_str()) == static_cast<int>(0x80020014u)); + Require(rmdir_nid_postfix(statLoop.string().c_str()) == -1 && *__error_nid_postfix() == 20); + const auto danglingDirectoryLink = root / "dangling-directory-link"; + Require(CreateSymbolicLinkW(danglingDirectoryLink.c_str(), L"missing-directory", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + for (const auto* linkPath : {&danglingLink, &danglingDirectoryLink, &directoryLink}) { + Require(sceKernelRmdir(linkPath->string().c_str()) == static_cast<int>(0x80020014u)); + Require(rmdir_nid_postfix(linkPath->string().c_str()) == -1 && *__error_nid_postfix() == 20); + const DWORD linkAttributes = GetFileAttributesW(linkPath->c_str()); + Require(linkAttributes != INVALID_FILE_ATTRIBUTES && (linkAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + } + const DWORD targetAttributes = GetFileAttributesW(targetDirectory.c_str()); + Require(targetAttributes != INVALID_FILE_ATTRIBUTES && (targetAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0); + Require(RemoveDirectoryW(danglingDirectoryLink.c_str()) != 0); Require(sceKernelTruncate_nid_postfix(statLoop.string().c_str(), 0) == static_cast<int>(0x8002003eu)); Require(truncate_nid_postfix(statLoop.string().c_str(), 0) == -1 && *__error_nid_postfix() == 62); FileStat followed{}; From b8636b11f7f65099b459916021981d479c93cb87 Mon Sep 17 00:00:00 2001 From: B-Howell <116104752+B-Howell@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:11:41 -0400 Subject: [PATCH 675/702] test(relinker): report the case and flag values when a masked store stub changes the flags masked_store_lowering failed twice under Rosetta on GitHub's macOS x86_64 runner (runs 40216485859 and 40438653738) with only "Masked store stub changed the flags", while the stored bytes, faults, vector and general registers, red zone and RSP all matched. The message now names the form, width, addressing mode, offset, mask pattern, fault count and the flags before and after, so the next occurrence shows which flags change and whether the probe faulted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/relinker/codegen/tests/MaskedStoreLoweringTests.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/tests/MaskedStoreLoweringTests.cpp b/core/relinker/codegen/tests/MaskedStoreLoweringTests.cpp index 5517011feb..d2acf6215f 100644 --- a/core/relinker/codegen/tests/MaskedStoreLoweringTests.cpp +++ b/core/relinker/codegen/tests/MaskedStoreLoweringTests.cpp @@ -5,6 +5,7 @@ #include <array> #include <cstddef> #include <cstdint> +#include <cstdio> #include <cstring> #include <iostream> #include <span> @@ -336,7 +337,13 @@ void execution() { require(stack[-8 - 128 + static_cast<int>(at)] == kSentinel, "Masked store stub wrote into the red zone"); require(std::memcmp(machine.Ymm, input.Ymm, sizeof(input.Ymm)) == 0, "Masked store stub changed a vector register"); require(std::memcmp(machine.Gpr, input.Gpr, sizeof(input.Gpr)) == 0, "Masked store stub changed a general register"); - require((machine.Flags & 0x8D5) == (input.Flags & 0x8D5), "Masked store stub changed the flags"); + if ((machine.Flags & 0x8D5) != (input.Flags & 0x8D5)) { + char detail[256]; + std::snprintf(detail, sizeof(detail), "Masked store stub changed the flags: %s %s, addressing %zu, offset %d, mask pattern 0x%02X, %d fault(s), flags 0x%03llX in, 0x%03llX out", + form.Name, wide ? "256-bit" : "128-bit", mode, offset, static_cast<unsigned>(pattern), static_cast<int>(faults), + static_cast<unsigned long long>(input.Flags & 0x8D5), static_cast<unsigned long long>(machine.Flags & 0x8D5)); + throw std::runtime_error(detail); + } require(machine.Stack == input.Stack, "Masked store stub did not restore RSP"); } } From b97100485700b420ce732db26b6228d36dc8fd9d Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Mon, 26 Oct 2026 09:13:18 -0400 Subject: [PATCH 676/702] feat(agc): inspect captured compute pipeline statistics --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +- .../Execution/src/VulkanDevice.cpp | 1 + .../tests/ComputePipelineInspectRun.cmake | 72 +++++++++++++++++++ .../tools/ComputePipelineReplay.cpp | 34 ++++++--- docs/dev/BUILD.md | 4 +- 5 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index a902a0a239..966bcaae5d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3754,6 +3754,11 @@ if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS) add_test(NAME agc_compute_pipeline_replay COMMAND agc_compute_pipeline_replay_tests) set_tests_properties(agc_compute_pipeline_replay PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77) add_dependencies(agc_compute_pipeline_replay_tests agc_compute_pipeline_replay agc_driver_branch_past_endpgm_tests) + add_test(NAME agc_compute_pipeline_inspection COMMAND ${CMAKE_COMMAND} + -DREPLAY=$<TARGET_FILE:agc_compute_pipeline_replay> + -DOUTPUT_ROOT=${CMAKE_CURRENT_BINARY_DIR}/capture-tests + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineInspectRun.cmake) + set_tests_properties(agc_compute_pipeline_inspection PROPERTIES TIMEOUT 60 SKIP_REGULAR_EXPRESSION "compute pipeline inspection: skipped unsupported executable statistics") add_test(NAME agc_compute_pipeline_capture_run COMMAND ${CMAKE_COMMAND} -DFIXTURE=$<TARGET_FILE:agc_driver_branch_past_endpgm_tests> -DREPLAY=$<TARGET_FILE:agc_compute_pipeline_replay> @@ -3761,7 +3766,7 @@ if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS) -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineCaptureRun.cmake) set_tests_properties(agc_compute_pipeline_capture_run PROPERTIES TIMEOUT 30 SKIP_REGULAR_EXPRESSION "compute pipeline capture run: skipped, no usable Vulkan device") if(WIN32) - set_tests_properties(agc_compute_pipeline_replay agc_compute_pipeline_capture_run PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + set_tests_properties(agc_compute_pipeline_replay agc_compute_pipeline_capture_run agc_compute_pipeline_inspection PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index dd6d622556..94f4bd035f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1110,6 +1110,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip phase = std::chrono::steady_clock::now(); CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), replay.cache, objects.module, objects.layout, capture.flags, capture.stageFlags, specialization.Info(), capture.requiredSubgroupSize, &objects.pipeline); result.pipelineMilliseconds = elapsed(phase); + if ((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) != 0) Graphics::LogPipelineStatistics_nid_no_patch(graphicsContext(), objects.pipeline); phase = std::chrono::steady_clock::now(); const auto getCache = state->DeviceFunction<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData"); std::size_t cacheBytes = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake new file mode 100644 index 0000000000..a8887b9939 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake @@ -0,0 +1,72 @@ +if(NOT DEFINED REPLAY OR NOT DEFINED OUTPUT_ROOT) + message(FATAL_ERROR "Missing replay tool or output root") +endif() +string(RANDOM LENGTH 12 ALPHABET 0123456789abcdef runId) +set(runDirectory "${OUTPUT_ROOT}/compute-inspect-${runId}") +file(MAKE_DIRECTORY "${runDirectory}") +set(report "${runDirectory}/inspection.json") +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=1" "${REPLAY}" --fixture --inspect --output "${report}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 +) +file(WRITE "${runDirectory}/inspection.log" "${output}\n${error}") +message("${error}") +if(error MATCHES "APS5_PIPELINE_STATS requires VK_KHR_pipeline_executable_properties") + message(STATUS "compute pipeline inspection: skipped unsupported executable statistics") + return() +endif() +if(NOT result STREQUAL "0" OR NOT EXISTS "${report}") + message(FATAL_ERROR "Inspection failed: ${result}\n${output}\n${error}") +endif() +file(READ "${report}" inspection) +string(JSON mode GET "${inspection}" mode) +string(JSON statistics GET "${inspection}" pipeline_statistics) +string(JSON timingType TYPE "${inspection}" milliseconds) +string(JSON flags GET "${inspection}" replay_pipeline_flags) +string(JSON dispatches GET "${inspection}" dispatch_count) +math(EXPR statisticsFlag "${flags} & 64") +if(NOT mode STREQUAL "inspection" OR NOT statistics OR NOT timingType STREQUAL "NULL" OR statisticsFlag EQUAL 0 OR NOT dispatches EQUAL 0) + message(FATAL_ERROR "Inspection report mixes counters and benchmark data: ${inspection}") +endif() +if(NOT error MATCHES "\\[pipeline-stats\\] executable=0 stages=") + message(FATAL_ERROR "Inspection did not query an executable: ${error}") +endif() +set(normalReport "${runDirectory}/normal.json") +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=0" "${REPLAY}" --fixture --output "${normalReport}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 +) +file(WRITE "${runDirectory}/normal.log" "${output}\n${error}") +message("${error}") +if(NOT result STREQUAL "0" OR NOT EXISTS "${normalReport}") + message(FATAL_ERROR "Normal replay failed: ${result}\n${output}\n${error}") +endif() +file(READ "${normalReport}" normal) +string(JSON mode GET "${normal}" mode) +string(JSON statistics GET "${normal}" pipeline_statistics) +string(JSON timingType TYPE "${normal}" milliseconds) +string(JSON originalIdentity GET "${inspection}" capture_identity) +string(JSON normalIdentity GET "${normal}" capture_identity) +string(JSON originalFlags GET "${inspection}" pipeline_flags) +string(JSON normalFlags GET "${normal}" pipeline_flags) +if(NOT mode STREQUAL "benchmark" OR statistics OR NOT timingType STREQUAL "OBJECT" OR NOT originalIdentity STREQUAL normalIdentity OR NOT originalFlags EQUAL normalFlags) + message(FATAL_ERROR "Replay mode changed the input identity or benchmark report") +endif() +function(expectFailure name statistics expected) + execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=${statistics}" "${REPLAY}" ${ARGN} + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 10 + ) + file(WRITE "${runDirectory}/${name}.log" "${output}\n${error}") + if(result STREQUAL "0" OR NOT error MATCHES "${expected}") + message(FATAL_ERROR "${name} was not rejected explicitly: ${result}\n${output}\n${error}") + endif() +endfunction() +expectFailure(benchmark-guard 1 "unset APS5_PIPELINE_STATS before benchmarking" --fixture) +expectFailure(missing-statistics 0 "requires APS5_PIPELINE_STATS=1" --fixture --inspect) +expectFailure(repeated-mode 1 "unknown or repeated argument" --fixture --inspect --inspect) +expectFailure(fixture-options 1 "cannot be combined with replay options" --write-fixture "${runDirectory}/fixture" --inspect) +message(STATUS "compute pipeline inspection: counters queried, input identity retained, timings separated and benchmark guards preserved") diff --git a/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp index 5baabe910d..e9e403659d 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp @@ -17,6 +17,7 @@ struct Options { std::filesystem::path output; std::filesystem::path fixtureDirectory; bool fixture = false; + bool inspect = false; }; Options Parse(int argc, char** argv) { @@ -32,12 +33,13 @@ Options Parse(int argc, char** argv) { else if (argument == "--output") value(result.output); else if (argument == "--write-fixture") value(result.fixtureDirectory); else if (argument == "--fixture" && !result.fixture) result.fixture = true; + else if (argument == "--inspect" && !result.inspect) result.inspect = true; else if (argument.starts_with("--") || !result.source.empty()) throw std::runtime_error("unknown or repeated argument: " + std::string(argument)); else result.source = argument; } const auto sources = static_cast<int>(!result.source.empty()) + static_cast<int>(result.fixture) + static_cast<int>(!result.fixtureDirectory.empty()); if (sources != 1) throw std::runtime_error("select a capture file, --fixture, or --write-fixture DIRECTORY"); - if (!result.fixtureDirectory.empty() && (!result.cacheIn.empty() || !result.cacheOut.empty() || !result.output.empty())) throw std::runtime_error("--write-fixture cannot be combined with replay options"); + if (!result.fixtureDirectory.empty() && (!result.cacheIn.empty() || !result.cacheOut.empty() || !result.output.empty() || result.inspect)) throw std::runtime_error("--write-fixture cannot be combined with replay options"); for (const auto& path : {result.cacheOut, result.output}) { if (!path.empty() && std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string()); } @@ -102,6 +104,7 @@ std::string Report(const Options& options, const AgcDriver::ComputePipelineCaptu << ",\n \"specialization_constants\":" << capture.specialization.size() << ",\n \"required_subgroup_size\":" << capture.requiredSubgroupSize << ",\n \"pipeline_flags\":" << capture.flags + << ",\n \"replay_pipeline_flags\":" << (capture.flags | (options.inspect ? static_cast<VkPipelineCreateFlags>(VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) : 0u)) << ",\n \"stage_flags\":" << capture.stageFlags << ",\n \"captured_device\":"; WriteDevice(output, capture); @@ -113,10 +116,15 @@ std::string Report(const Options& options, const AgcDriver::ComputePipelineCaptu << ",\n \"cache_export_path\":" << Quote(options.cacheOut.string()) << ",\n \"cache_export_bytes\":" << (options.cacheOut.empty() ? 0u : replay.cache.size()) << ",\n \"driver_cache_state\":\"uncontrolled\"" - << ",\n \"dispatch_count\":0,\n \"pipeline_statistics\":false" - << ",\n \"milliseconds\":{\"shader_module\":" << replay.moduleMilliseconds - << ",\"layout\":" << replay.layoutMilliseconds << ",\"pipeline_create\":" << replay.pipelineMilliseconds - << ",\"cache\":" << replay.cacheMilliseconds << ",\"replay_total\":" << replay.totalMilliseconds << "}\n}\n"; + << ",\n \"dispatch_count\":0,\n \"pipeline_statistics\":" << (options.inspect ? "true" : "false") + << ",\n \"mode\":" << Quote(options.inspect ? "inspection" : "benchmark"); + if (options.inspect) { + output << ",\n \"milliseconds\":null\n}\n"; + } else { + output << ",\n \"milliseconds\":{\"shader_module\":" << replay.moduleMilliseconds + << ",\"layout\":" << replay.layoutMilliseconds << ",\"pipeline_create\":" << replay.pipelineMilliseconds + << ",\"cache\":" << replay.cacheMilliseconds << ",\"replay_total\":" << replay.totalMilliseconds << "}\n}\n"; + } return output.str(); } @@ -124,8 +132,9 @@ std::string Report(const Options& options, const AgcDriver::ComputePipelineCaptu int main(int argc, char** argv) { if (argc == 2 && std::string_view(argv[1]) == "--help") { - std::cout << "agc_compute_pipeline_replay CAPTURE|--fixture [--cache-in FILE] [--cache-out FILE] [--output FILE]\n" + std::cout << "agc_compute_pipeline_replay CAPTURE|--fixture [--cache-in FILE] [--cache-out FILE] [--output FILE] [--inspect]\n" "agc_compute_pipeline_replay --write-fixture DIRECTORY\n" + "--inspect requires APS5_PIPELINE_STATS=1, prints executable counters to stderr and omits benchmark timings.\n" "Compiles one compute pipeline without dispatch. Fresh application cache does not clear driver caches.\n"; return 0; } @@ -136,12 +145,19 @@ int main(int argc, char** argv) { return 0; } const auto* statistics = std::getenv("APS5_PIPELINE_STATS"); - if (statistics != nullptr && std::string_view(statistics) != "0" && *statistics != '\0') throw std::runtime_error("unset APS5_PIPELINE_STATS before benchmarking"); + const bool statisticsEnabled = statistics != nullptr && std::string_view(statistics) != "0" && *statistics != '\0'; + if (options.inspect && !statisticsEnabled) throw std::runtime_error("--inspect requires APS5_PIPELINE_STATS=1"); + if (!options.inspect && statisticsEnabled) throw std::runtime_error("unset APS5_PIPELINE_STATS before benchmarking"); const auto capture = options.fixture ? AgcDriver::ComputePipelineFixture() : AgcDriver::ComputePipelineCapture::Read(options.source); - if ((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) != 0) throw std::runtime_error("capture requests pipeline statistics; capture again with APS5_PIPELINE_STATS disabled"); + if (!options.inspect && (capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) != 0) throw std::runtime_error("capture requests pipeline statistics; capture again with APS5_PIPELINE_STATS disabled"); const auto cache = ReadCache(options.cacheIn); AgcDriver::VulkanDevice device(AgcDriver::VulkanDevice::CompileOnly{}); - const auto replay = device.ReplayComputePipeline(capture, cache); + const auto replay = [&] { + if (!options.inspect) return device.ReplayComputePipeline(capture, cache); + auto inspection = capture; + inspection.flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR; + return device.ReplayComputePipeline(inspection, cache); + }(); if (!options.cacheOut.empty()) WriteBytes(options.cacheOut, replay.cache); const auto report = Report(options, capture, replay); if (!options.output.empty()) WriteBytes(options.output, std::as_bytes(std::span(report.data(), report.size()))); diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 1154ea812c..d7b8b0947d 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -207,11 +207,13 @@ build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp - build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-in pipeline.cache --output imported.json ``` +Use `APS5_PIPELINE_STATS=1 agc_compute_pipeline_replay capture.aps5cp --inspect --output inspection.json` to inspect the captured pipeline without dispatching it. This requires `VK_KHR_pipeline_executable_properties`, requests statistics for this replay, and prints the driver's executable and counter names, values and descriptions to stderr. Available counters depend on the driver. The JSON report marks `mode` as `inspection`, sets `pipeline_statistics` to true, records the effective `replay_pipeline_flags`, and sets `milliseconds` to null. The captured identity and original flags are retained. Inspection output is not a compilation benchmark. Replay without `--inspect` continues to reject enabled statistics or captures carrying the statistics flag. `--inspect` cannot be combined with `--write-fixture`. + On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs/unpatched` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Compile-only devices also enable supported partially-bound, variable-count and runtime-array descriptor features to replay captured layouts; ordinary runtime devices are unchanged. Binding flags are preserved independently of update-after-bind. Replay accepts the unconditional Vulkan 1.0 capabilities listed in the pinned [Vulkan registry](https://github.com/KhronosGroup/Vulkan-Headers/blob/2fa203425eb4af9dfc6b03f97ef72b0b5bcb8350/registry/vk.xml); other capabilities must be enabled by the device target. Non-Vulkan capabilities such as Kernel and unknown capabilities fail before pipeline creation. Unsupported features, non-highest variable bindings and counts exceeding layout support fail explicitly. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. Each invocation creates one pipeline. With no `--cache-in`, its application pipeline cache starts empty; `--cache-in` imports a raw cache previously written by `--cache-out` (at most 64 MiB), and `--cache-out` exports the resulting cache. The title's wrapped `vk-pipelines-*.bin` format is not accepted. It does not load or save the title's usual pipeline cache. NVIDIA and other driver caches remain uncontrolled: a fresh application cache is not a cold driver cache. JSON reports the capture identity, GPU/driver identifiers, word count, cache policy and separate wall times for shader module, layouts, pipeline creation and cache work. Device initialization is outside these timings. Pipeline statistics captures and `APS5_PIPELINE_STATS` are refused because they change compilation cost. -`--fixture` compiles a tiny synthetic compute shader with specialization, sparse descriptor binding and push constant metadata; `--write-fixture DIRECTORY` saves it without opening Vulkan. It performs no GPU work and does not reproduce a title's compile workload. With `BUILD_TESTING=ON`, `agc_compute_pipeline_capture` checks serialization and rejected inputs. With SPIRV-Tools also enabled, `agc_compute_pipeline_replay` compiles the fixture with fresh and imported application caches, verifies cache rejection, and checks required subgroup support. `agc_compute_pipeline_capture_run` verifies the existing branch-past-endpgm compute fixture's output with capture disabled and enabled, then replays the captured pipelines and checks their identities and metadata. Each test has a 30-second timeout. Runtime tests skip when Vulkan is absent; set `ANYPS5_REQUIRE_VULKAN=1` to require execution. There is no performance pass threshold. +`--fixture` compiles a tiny synthetic compute shader with specialization, sparse descriptor binding and push constant metadata; `--write-fixture DIRECTORY` saves it without opening Vulkan. It performs no GPU work and does not reproduce a title's compile workload. With `BUILD_TESTING=ON`, `agc_compute_pipeline_capture` checks serialization and rejected inputs. With SPIRV-Tools also enabled, `agc_compute_pipeline_replay` compiles the fixture with fresh and imported application caches, verifies cache rejection, and checks required subgroup support. `agc_compute_pipeline_capture_run` verifies the existing branch-past-endpgm compute fixture's output with capture disabled and enabled, then replays the captured pipelines and checks their identities and metadata. `agc_compute_pipeline_inspection` checks executable counter queries, diagnostic JSON, retained capture identity, and benchmark-mode guards; its timeout is 60 seconds. The other tests have 30-second timeouts. Runtime tests skip when Vulkan is absent; set `ANYPS5_REQUIRE_VULKAN=1` to require execution. There is no performance pass threshold. ## PM4 submission replay From cc59cb7b936a07fccbb751c8787a20db073e4e68 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Mon, 26 Oct 2026 09:30:44 -0400 Subject: [PATCH 677/702] test(agc): skip pipeline inspection when Vulkan is unavailable --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 ++- .../tests/ComputePipelineInspectRun.cmake | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 966bcaae5d..1fa936a596 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3756,9 +3756,10 @@ if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS) add_dependencies(agc_compute_pipeline_replay_tests agc_compute_pipeline_replay agc_driver_branch_past_endpgm_tests) add_test(NAME agc_compute_pipeline_inspection COMMAND ${CMAKE_COMMAND} -DREPLAY=$<TARGET_FILE:agc_compute_pipeline_replay> + -DVULKAN_PROBE=$<TARGET_FILE:agc_compute_pipeline_replay_tests> -DOUTPUT_ROOT=${CMAKE_CURRENT_BINARY_DIR}/capture-tests -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineInspectRun.cmake) - set_tests_properties(agc_compute_pipeline_inspection PROPERTIES TIMEOUT 60 SKIP_REGULAR_EXPRESSION "compute pipeline inspection: skipped unsupported executable statistics") + set_tests_properties(agc_compute_pipeline_inspection PROPERTIES TIMEOUT 60 SKIP_REGULAR_EXPRESSION "compute pipeline inspection: skipped") add_test(NAME agc_compute_pipeline_capture_run COMMAND ${CMAKE_COMMAND} -DFIXTURE=$<TARGET_FILE:agc_driver_branch_past_endpgm_tests> -DREPLAY=$<TARGET_FILE:agc_compute_pipeline_replay> diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake index a8887b9939..29396980ae 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake @@ -4,6 +4,18 @@ endif() string(RANDOM LENGTH 12 ALPHABET 0123456789abcdef runId) set(runDirectory "${OUTPUT_ROOT}/compute-inspect-${runId}") file(MAKE_DIRECTORY "${runDirectory}") +if(NOT DEFINED ENV{ANYPS5_REQUIRE_VULKAN} AND DEFINED VULKAN_PROBE) + execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=0" "${VULKAN_PROBE}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE probeResult OUTPUT_VARIABLE probeOutput ERROR_VARIABLE probeError TIMEOUT 20 + ) + file(WRITE "${runDirectory}/vulkan-probe.log" "${probeOutput}\n${probeError}") + if(probeResult STREQUAL "77") + message(STATUS "compute pipeline inspection: skipped, no usable Vulkan device") + return() + endif() +endif() set(report "${runDirectory}/inspection.json") execute_process( COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=1" "${REPLAY}" --fixture --inspect --output "${report}" From 0f8ac231b0c669f88f7cdc47689b316036c25ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <adriadam10@gmail.com> Date: Wed, 28 Oct 2026 08:27:19 +0100 Subject: [PATCH 678/702] test(agc): report the device limitation when pipeline inspection skips Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- .../prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake index 29396980ae..61a2b8d0c6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineInspectRun.cmake @@ -25,7 +25,7 @@ execute_process( file(WRITE "${runDirectory}/inspection.log" "${output}\n${error}") message("${error}") if(error MATCHES "APS5_PIPELINE_STATS requires VK_KHR_pipeline_executable_properties") - message(STATUS "compute pipeline inspection: skipped unsupported executable statistics") + message(STATUS "compute pipeline inspection: skipped, the device reports no executable statistics") return() endif() if(NOT result STREQUAL "0" OR NOT EXISTS "${report}") From 2b349ff15655fa5acc8916055c93a84d71eb4f18 Mon Sep 17 00:00:00 2001 From: Cesar Mora <augusto.morag@gmail.com> Date: Mon, 26 Oct 2026 11:18:59 -0600 Subject: [PATCH 679/702] perf(agc): repeat a draw shape's resident targets and pipeline instead of looking them up again Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Execution/include/Driver/Driver.hpp | 2 +- .../Execution/include/VulkanDevice.hpp | 2 +- .../Execution/src/Driver/Draw/Draw.cpp | 10 +- .../Execution/src/VulkanDevice.cpp | 4 +- .../libSceAgcDriver/Graphics/include/Draw.hpp | 2 +- .../Graphics/include/Pipeline.hpp | 3 +- .../Graphics/include/ShaderResources.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 160 +++++++++++++++++- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 24 ++- .../Graphics/src/ShaderResources.cpp | 25 +++ docs/dev/ENVIRONMENT.md | 2 + 11 files changed, 219 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 12d373adc4..62d17354e5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -276,7 +276,7 @@ private: void noteForeignWriter(std::uint64_t begin, std::uint64_t end, std::uint32_t queue); void noteDrawWriters(std::span<const Graphics::CompiledShader> stages, std::uint32_t queue); static std::vector<std::pair<std::uint64_t, std::uint64_t>> drawWriteRanges(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> stages); - void commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace = {}); + void commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace = {}, std::uint64_t shape = 0); bool labelPacketQueues(std::span<const std::uint32_t> packet, std::uint32_t opcode); bool enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue); bool enqueueDmaPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 1e380f74b4..2b76acfa94 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -344,7 +344,7 @@ public: using DrawTraceContext = Graphics::DrawTraceContext; // `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, direct draw // built for its draw-cache entry (design_cpu_final M8); null otherwise. - void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr); + void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr, std::uint64_t shape = 0); bool DrawInputsInPlace() const; std::optional<std::string> KnownDrawRejection(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> shaders) const; void ColorMetadataPass(const Graphics::ColorMetadataPass& pass); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index e26442387b..0069a51f06 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -1245,10 +1245,10 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { const auto commitPacket = tail.packetOpcode; const auto epoch = DrawFinisher::OnFinisherThread() ? tail.epoch : DrawPipeline::EpochToken().load(std::memory_order_relaxed); auto reads = QueuedDrawReadRanges(drawParameters, stages); - auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, trace = trace, writablePrograms = std::move(writablePrograms), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)]() mutable { + auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, shape = tail.shapeKey, commitQueue, commitPacket, epoch, trace = trace, writablePrograms = std::move(writablePrograms), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)]() mutable { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(commitPacket, commitQueue); - commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey, trace); + commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey, trace, shape); }; auto held = std::make_shared<decltype(commit)>(std::move(commit)); DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes), 0, {}, std::move(reads), true); @@ -1270,14 +1270,14 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, VulkanDevice::RecipeKind::Draw); } std::shared_ptr<const DrawRecipe> built; - localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr); + localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr, tail.shapeKey); phaseTiming.Phase(DrawRowGraphics); if (built != nullptr) attachDrawRecipe(drawKey, recipeStages, std::move(built)); timing.Mark("draw_and_resource_release"); return drawn(); } -void Driver::commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace) { +void Driver::commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace, std::uint64_t shape) { const auto* tracePtr = trace.token != 0 ? &trace : nullptr; GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); std::lock_guard gpuLock(GuestMemory::GpuMutex()); @@ -1296,7 +1296,7 @@ void Driver::commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, VulkanDevice::RecipeKind::Draw); } std::shared_ptr<const DrawRecipe> built; - localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr); + localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr, shape); if (built != nullptr) attachDrawRecipe(drawKey, recipeStages, std::move(built)); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index dd6d622556..afad176d75 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4281,7 +4281,7 @@ bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stage return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), group); } -void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe, const DrawTraceContext* trace) { +void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe, const DrawTraceContext* trace, std::uint64_t shape) { startJournalReplay(); for (const auto& shader : shaders) { require(shader.program != nullptr, "missing compiled shader"); @@ -4292,7 +4292,7 @@ void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParamete static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; if (traceDraws) APS5_LOG_OUT("VulkanDevice::Draw indices=%u instances=%u indexSize=%u address=0x%llx shaders=%zu colorTarget=%u", draw.indexCount, draw.instanceCount, draw.indexSize, static_cast<unsigned long long>(draw.indexAddress), shaders.size(), static_cast<unsigned>(graphics.hasColorTarget)); const auto context = graphicsContext(); - Graphics::Draw(context, graphics, draw, shaders, snapshots, recipe, trace); + Graphics::Draw(context, graphics, draw, shaders, snapshots, recipe, trace, shape); if (traceDraws) APS5_LOG_CHARS_OUT("VulkanDevice::Draw complete"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index 14cf92c7f0..4694fd5503 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -25,7 +25,7 @@ struct DrawTraceContext { // `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, non-indirect draw // built for its draw-cache entry (design_cpu_final M8; null otherwise, and always under // APS5_NO_DRAW_RECIPE=1). -void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr); +void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr, std::uint64_t shape = 0); struct PixelFunctionLdsLayouts { VkDescriptorSetLayout empty = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index d246e5b4f1..067d251c70 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -115,7 +115,8 @@ private: // APS5_NO_PIPELINE_CACHE=1 builds one per draw as before. std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); // `created` reports whether this call built the pipeline (false: served from the store or private). -std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr); +std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr, std::uint64_t* storeHash = nullptr); +std::shared_ptr<Pipeline> TouchCachedPipeline(const Context& context, std::uint64_t hash, const std::weak_ptr<Pipeline>& expected); // Cumulative pipeline store outcomes since start-up (the [pipecache] line reports 10 s windows). struct PipelineStoreCounters { std::uint64_t hits = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index a65300d107..a65ab38f73 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -65,6 +65,7 @@ std::shared_ptr<StorageTexture> CachedStorageLayers(const Context& context, cons // would return); a true answer counts as a use for the cache's eviction order, as a lookup would. bool StorageImageCached(const Context& context, const StorageTexture* image); bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddress); +std::shared_ptr<StorageTexture> RepeatStorageSurface(const Context& context, const GuestTextureResource& resource, const std::shared_ptr<StorageTexture>& stored); // Defined in Texture.cpp beside the pending-results registry, for the fast Revalidate below: whether // a storage image other than `except` has results pending in [address, address + bytes). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 904f02dd0e..cf991161d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2515,6 +2515,7 @@ struct PreparedDraw { PreparedDraw& operator=(const PreparedDraw&) = delete; std::shared_ptr<const DrawRecipe>* recipeOut = nullptr; const DrawTraceContext* trace = nullptr; + std::uint64_t shape = 0; std::vector<std::uint32_t> stencilReferences; std::optional<PerformanceTimer> timing; DrawTimer timer; @@ -2547,6 +2548,137 @@ struct PreparedDraw { std::optional<DepthSampleLocations> depthLocations; }; +bool ShapeMemoEnabled() { + static const bool enabled = std::getenv("APS5_NO_SHAPE_MEMO") == nullptr; + return enabled; +} + +bool VerifyShapeMemo() { + static const bool verify = std::getenv("APS5_VERIFY_SHAPE_MEMO") != nullptr; + return verify; +} + +void noteShapeMemoVerified(bool pipeline) { + static std::atomic<std::uint64_t> targets{0}; + static std::atomic<std::uint64_t> pipelines{0}; + static std::atomic<std::int64_t> last{0}; + (pipeline ? pipelines : targets).fetch_add(1, std::memory_order_relaxed); + const auto now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now().time_since_epoch()).count(); + auto previous = last.load(std::memory_order_relaxed); + if (now - previous < 10 || !last.compare_exchange_strong(previous, now)) return; + std::fprintf(stderr, "[shape-memo] verified %llu targets and %llu pipelines\n", static_cast<unsigned long long>(targets.load()), static_cast<unsigned long long>(pipelines.load())); +} + +struct ShapeChoice { + VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_UNDEFINED; + std::uint32_t fragmentOutputs = 0; + std::vector<std::uint64_t> stages; + std::vector<VkVertexInputBindingDescription> bindings; + std::vector<VkVertexInputAttributeDescription> attributes; + std::vector<std::uint32_t> mainKey; + std::vector<std::uint32_t> fragmentKey; + std::uint64_t hash = 0; + std::weak_ptr<Pipeline> pipeline; +}; + +struct ShapeEntry { + VkDevice device = VK_NULL_HANDLE; + std::vector<std::weak_ptr<StorageTexture>> targets; + std::vector<ShapeChoice> choices; +}; + +struct ShapeMemoStorage {}; +constexpr std::size_t ShapeMemoEntries = 4096; +constexpr std::size_t ShapeMemoChoices = 4; + +std::unordered_map<std::uint64_t, ShapeEntry>& shapeMemo() { + return HostThreadLocal<std::unordered_map<std::uint64_t, ShapeEntry>, ShapeMemoStorage>(); +} + +const ShapeEntry* findShape(const Context& context, std::uint64_t shape) { + if (shape == 0 || !ShapeMemoEnabled()) return nullptr; + const auto& entries = shapeMemo(); + const auto found = entries.find(shape); + return found == entries.end() || found->second.device != context.device ? nullptr : &found->second; +} + +std::shared_ptr<StorageTexture> shapeTarget(const Context& context, std::uint64_t shape, std::size_t index) { + const auto* entry = findShape(context, shape); + return entry == nullptr || index >= entry->targets.size() ? nullptr : entry->targets[index].lock(); +} + +bool sameStages(std::span<const std::uint64_t> kept, std::span<const CompiledShader> shaders) { + if (kept.size() != shaders.size() * 4u + 1u) return false; + std::size_t at = 0; + for (const auto& shader : shaders) { + const auto identity = shader.program->Identity(); + if (identity.Empty() || kept[at] != static_cast<std::uint64_t>(shader.stage) || kept[at + 1] != identity.low || kept[at + 2] != identity.high || kept[at + 3] != shader.pushConstantOffset) return false; + at += 4; + } + return kept[at] == PushConstantStages(shaders); +} + +bool sameChoice(const ShapeChoice& choice, const VertexInputLayout& input, const PipelineSetLayouts& layouts, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, std::uint32_t fragmentOutputs) { + const auto sameBinding = [](const VkVertexInputBindingDescription& a, const VkVertexInputBindingDescription& b) { return a.binding == b.binding && a.stride == b.stride && a.inputRate == b.inputRate; }; + const auto sameAttribute = [](const VkVertexInputAttributeDescription& a, const VkVertexInputAttributeDescription& b) { return a.location == b.location && a.binding == b.binding && a.format == b.format && a.offset == b.offset; }; + return choice.attachmentLayout == attachmentLayout && choice.fragmentOutputs == fragmentOutputs && std::ranges::equal(choice.bindings, input.bindings, sameBinding) && std::ranges::equal(choice.attributes, input.attributes, sameAttribute) && std::ranges::equal(choice.mainKey, layouts.mainKey) && std::ranges::equal(choice.fragmentKey, layouts.fragmentKey) && sameStages(choice.stages, shaders); +} + +std::shared_ptr<Pipeline> shapePipeline(const Context& context, std::uint64_t shape, const VertexInputLayout& input, const PipelineSetLayouts& layouts, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, std::uint32_t fragmentOutputs) { + const auto* entry = findShape(context, shape); + if (entry == nullptr) return nullptr; + for (const auto& choice : entry->choices) { + if (!sameChoice(choice, input, layouts, shaders, attachmentLayout, fragmentOutputs)) continue; + const auto hash = choice.hash; + const auto expected = choice.pipeline; + return TouchCachedPipeline(context, hash, expected); + } + return nullptr; +} + +void rememberShape(const Context& context, std::uint64_t shape, std::span<const TargetBinding> targets, const std::shared_ptr<Pipeline>* pipeline, std::uint64_t hash, const VertexInputLayout& input, const PipelineSetLayouts& layouts, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, std::uint32_t fragmentOutputs) { + if (shape == 0 || !ShapeMemoEnabled()) return; + auto& entries = shapeMemo(); + if (entries.size() >= ShapeMemoEntries && !entries.contains(shape)) entries.clear(); + auto& entry = entries[shape]; + if (entry.device != context.device) { + entry = {}; + entry.device = context.device; + } + entry.targets.resize(targets.size()); + for (std::size_t index = 0; index < targets.size(); ++index) { + const auto& resident = targets[index].resident; + if (resident == nullptr || targets[index].color.arrayLastSlice != 0) entry.targets[index].reset(); + else if (entry.targets[index].owner_before(resident) || resident.owner_before(entry.targets[index])) entry.targets[index] = resident; + } + if (pipeline == nullptr || hash == 0) return; + auto& choices = entry.choices; + const auto kept = std::ranges::find_if(choices, [&](const ShapeChoice& choice) { return sameChoice(choice, input, layouts, shaders, attachmentLayout, fragmentOutputs); }); + if (kept != choices.end()) { + kept->hash = hash; + kept->pipeline = *pipeline; + std::rotate(choices.begin(), kept, std::next(kept)); + return; + } + ShapeChoice choice; + choice.attachmentLayout = attachmentLayout; + choice.fragmentOutputs = fragmentOutputs; + for (const auto& shader : shaders) { + const auto identity = shader.program->Identity(); + if (identity.Empty()) return; + choice.stages.insert(choice.stages.end(), {static_cast<std::uint64_t>(shader.stage), identity.low, identity.high, shader.pushConstantOffset}); + } + choice.stages.push_back(PushConstantStages(shaders)); + choice.bindings = input.bindings; + choice.attributes = input.attributes; + choice.mainKey.assign(layouts.mainKey.begin(), layouts.mainKey.end()); + choice.fragmentKey.assign(layouts.fragmentKey.begin(), layouts.fragmentKey.end()); + choice.hash = hash; + choice.pipeline = *pipeline; + if (choices.size() >= ShapeMemoChoices) choices.pop_back(); + choices.insert(choices.begin(), std::move(choice)); +} + bool prepareDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, PreparedDraw& prepared) { auto*& recipeOut = prepared.recipeOut; auto& stencilReferences = prepared.stencilReferences; @@ -2650,7 +2782,13 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara Require(resident->Attachable(), "storage format cannot be a color attachment"); return resident; } - auto resident = CachedStorageSurface(context, SurfaceForTarget(color)); + const auto surface = SurfaceForTarget(color); + auto resident = RepeatStorageSurface(context, surface, shapeTarget(context, prepared.shape, index)); + if (resident != nullptr && VerifyShapeMemo()) { + Require(CachedStorageSurface(context, surface) == resident, "APS5_VERIFY_SHAPE_MEMO: the shape memo's resident target differs from the storage cache's"); + noteShapeMemoVerified(false); + } + if (resident == nullptr) resident = CachedStorageSurface(context, surface); Require(resident->Attachable(), "storage format cannot be a color attachment"); Require(color.mipCount > 1 || color.depth > 1 || resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); return resident; @@ -2910,7 +3048,22 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara stencilReplay.emplace_back(reference, std::move(selectedPipeline)); } auto& pipeline = prepared.pipeline; - pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second; + const auto attachmentLayout = lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + if (!stencilReplay.empty()) { + pipeline = stencilReplay.front().second; + } else { + const bool memoized = stencilReferences.empty() && std::none_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.proxied || binding.multisampled; }); + const auto layouts = resources->SetLayouts(); + std::uint64_t storeHash = 0; + if (memoized) pipeline = shapePipeline(context, prepared.shape, inputs.VertexLayout(), layouts, shaders, attachmentLayout, inputs.fragmentOutputs); + if (pipeline != nullptr && VerifyShapeMemo()) { + Require(CachedPipeline(context, pipelineState, inputs.VertexLayout(), layouts, shaders, attachmentLayout) == pipeline, "APS5_VERIFY_SHAPE_MEMO: the shape memo's pipeline differs from the pipeline store's"); + noteShapeMemoVerified(true); + } + const bool repeated = pipeline != nullptr; + if (!repeated) pipeline = CachedPipeline(context, pipelineState, inputs.VertexLayout(), layouts, shaders, attachmentLayout, nullptr, &storeHash); + if (memoized) rememberShape(context, prepared.shape, targets, repeated ? nullptr : &pipeline, storeHash, inputs.VertexLayout(), layouts, shaders, attachmentLayout, inputs.fragmentOutputs); + } // 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. auto& owners = prepared.owners; @@ -3374,7 +3527,7 @@ void drawTessellationGeometry(const Context& context, const State& state, const Draw(context, meshed, captured, second, snapshots, nullptr, trace); } -void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut, const DrawTraceContext* trace) { +void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut, const DrawTraceContext* trace, std::uint64_t shape) { const DepthCopySourceScope depthCopySource(state.depthCopySource); if (state.stages.tessellationGeometry) { drawTessellationGeometry(context, state, draw, shaders, snapshots, trace); @@ -3384,6 +3537,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& PreparedDraw prepared(profile); prepared.recipeOut = recipeOut; prepared.trace = trace; + prepared.shape = shape; if (prepareDraw(context, state, draw, shaders, snapshots, prepared)) recordPreparedDraw(context, state, draw, shaders, prepared); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 353e5a7d1e..c0779ee61c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -1228,9 +1228,10 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st return CachedPipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout); } -std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, bool* created) { +std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, bool* created, std::uint64_t* storeHash) { static const bool disabled = std::getenv("APS5_NO_PIPELINE_CACHE") != nullptr; if (created != nullptr) *created = false; + if (storeHash != nullptr) *storeHash = 0; if (disabled) return std::make_shared<Pipeline>(context, state, vertexInput, resources, shaders, attachmentLayout); auto& store = Pipelines(); struct PipelineKeyStorage {}; @@ -1270,6 +1271,7 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st if (auto pipeline = lookup(collision)) { ++store.hits; ++store.totals.hits; + if (storeHash != nullptr) *storeHash = hash; return pipeline; } if (collision) { @@ -1290,7 +1292,10 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st { std::lock_guard lock(store.mutex); bool collision = false; - if (auto raced = lookup(collision)) return raced; + if (auto raced = lookup(collision)) { + if (storeHash != nullptr) *storeHash = hash; + return raced; + } if (collision) { ++store.uncached; ++store.totals.uncached; @@ -1325,12 +1330,27 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st ++store.evicted; ++store.totals.evicted; } + if (storeHash != nullptr && store.index.contains(hash)) *storeHash = hash; } if (created != nullptr) *created = true; if (PipelineJournalEnabled() && !PipelineJournalReplaying() && state.tessellationCapture == nullptr) RecordGraphicsPipeline(MakeGraphicsPipelineRecipe(state, vertexInput, resources, shaders, attachmentLayout), shaders); return pipeline; } +std::shared_ptr<Pipeline> TouchCachedPipeline(const Context& context, std::uint64_t hash, const std::weak_ptr<Pipeline>& expected) { + auto& store = Pipelines(); + std::lock_guard lock(store.mutex); + reportPipelines(store); + const auto found = store.index.find(hash); + if (found == store.index.end()) return nullptr; + const auto it = found->second; + if (it->pipeline.owner_before(expected) || expected.owner_before(it->pipeline) || !alive(*it, context)) return nullptr; + store.entries.splice(store.entries.end(), store.entries, it); + ++store.hits; + ++store.totals.hits; + return it->pipeline; +} + void BuildPipelineStoreKey(std::vector<std::byte>& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) { pipelineKey(key, context, state, vertexInput, resources, shaders, attachmentLayout); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index a57229370e..51fd4ac377 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1545,6 +1545,31 @@ bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddres return !MetadataMoved(image, resource); } +std::shared_ptr<StorageTexture> RepeatStorageSurface(const Context& context, const GuestTextureResource& requested, const std::shared_ptr<StorageTexture>& stored) { + static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; + if (disabled || stored == nullptr) return nullptr; + const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); + Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); + RequireNoMultisampleTarget(viewed.baseAddress, 1u, "a storage image access"); + if (DepthSurfaceStorage(context, {}, viewed, 0) != nullptr) return nullptr; + if (DepthPlanesStale()) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(DescribeSurface(viewed).guestBytes), DepthPlaneScope::Overlapped)); + if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); + const bool profile = LookupOutcomes::Profiled(); + const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + const auto resource = StorageSurface(context, viewed); + auto& cache = StorageTextures(); + std::lock_guard lock(cache.mutex); + const CacheHold held(storageCacheHeld); + if (resource.mipCount > AllocatedLevels(resource)) return nullptr; + const auto it = findStorageByImage(cache, context.device, stored.get()); + if (it == cache.entries.end() || MetadataMoved(*it->texture, resource)) return nullptr; + it->texture->Refresh(); + cache.entries.splice(cache.entries.begin(), cache.entries, it); + TextureCounts().storageHits.fetch_add(1, std::memory_order_relaxed); + if (profile) LookupOutcomes::Add(LookupOutcomes::StorageHit, start); + return it->texture; +} + namespace { const char* roleName(ShaderRecompiler::DescriptorRole role) { diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index b74ec48890..30e9ec9314 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -327,6 +327,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_SAMPLED_FROM_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps every sampled texture a CPU snapshot instead of viewing the surface's storage image | | `APS5_NO_SAMPLER_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | creates a sampler per binding instead of caching them | | `APS5_NO_SEPARATE_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a queued label enter the recorded table and replace a recorded entry of its dword | +| `APS5_NO_SHAPE_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | looks up every draw's resident targets and pipeline anew instead of repeating the ones an earlier draw of the same register shape used | | `APS5_NO_SINGLE_PASS_COLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | walks the written pages once without clearing them and, when any were written, a second time clearing them, instead of one cleared pass | | `APS5_NO_SITE_SAMPLING` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables per-site sampling of write evidence | | `APS5_NO_SNAPSHOT_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | skips the live compare that proves a captured snapshot still equals the guest bytes | @@ -475,6 +476,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_VERIFY_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | makes the ordinary prepare path beside a recipe hit and compares the answers | | `APS5_VERIFY_REGISTER_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp` | | checks the incremental register fingerprint against the full state | | `APS5_VERIFY_SET_LAYOUTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | checks the draw's set layouts against those described from the shaders | +| `APS5_VERIFY_SHAPE_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | looks up the resident targets and pipeline of every shape memo hit anew as well, throws when they differ and prints `[shape-memo]` counts of the checked hits every 10 s | | `APS5_WAIT_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | always takes the GPU mutex on a wait (no unlocked label lookup) | | `APS5_WAIT_LOCK_ENTRY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | takes the GPU mutex blocking on the entry instead of trying it | | `APS5_WATCH_MEMORY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | watches named guest ranges (`<hex address>:<hex bytes>[,...]`) across dispatches; needs `APS5_SYNC_DISPATCH=1` | From 46366e060ab24da215c591c9e07287314dbed28c Mon Sep 17 00:00:00 2001 From: Cesar Mora <augusto.morag@gmail.com> Date: Mon, 26 Oct 2026 11:54:16 -0600 Subject: [PATCH 680/702] test(agc): a touched pipeline is returned as a store hit and stays recently used Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index a902a0a239..833ab62bb7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2918,6 +2918,7 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_journal_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build) add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool) + add_test(NAME agc_pipeline_store_touch COMMAND agc_driver_graphics_tests --pipeline-store-touch) add_test(NAME agc_pipeline_store_growth COMMAND agc_driver_graphics_tests --pipeline-store-growth) add_test(NAME agc_pipeline_store_fixed_bound COMMAND agc_driver_graphics_tests --pipeline-store-growth) add_test(NAME agc_pipeline_store_replay_growth COMMAND agc_driver_graphics_tests --pipeline-store-replay-growth) @@ -2925,12 +2926,12 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_store_fixed_bound PROPERTIES ENVIRONMENT "APS5_PIPELINE_STORE_ENTRIES=1024") set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") - set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_store_lazy_buffer_pool agc_pipeline_store_touch PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) add_test(NAME agc_pipeline_journal_extended_paths COMMAND agc_driver_graphics_tests --pipeline-journal-extended-paths) set_tests_properties(agc_pipeline_journal_extended_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_journal_long_paths agc_pipeline_journal_extended_paths PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") - set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool agc_pipeline_store_touch agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index a0a6f51f1c..21accfeb6d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -11952,6 +11952,57 @@ void pipelineStoreGrowthTests(bool fixed) { Require(mock.live == 0, "pipeline store growth tests leaked Vulkan objects"); } +void pipelineStoreTouchTests() { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle<VkDevice>(); + context.graphicsPipelineLibrary = true; + context.pipelineLinkTimeOptimization = false; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 401; + fragment.variantId = 402; + const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array<std::uint32_t, 1> mainKey{51}, fragmentKey{52}; + const PipelineSetLayouts resources{makeHandle<VkDescriptorSetLayout>(), makeHandle<VkDescriptorSetLayout>(), true, mainKey, fragmentKey}; + const auto layout = [](std::uint32_t configuration) { + VertexInputLayout input; + input.bindings.push_back({0, 4u * (configuration + 1u), VK_VERTEX_INPUT_RATE_VERTEX}); + return input; + }; + std::uint64_t hash = 0; + std::weak_ptr<Pipeline> touched; + std::weak_ptr<Pipeline> other; + { + auto first = CachedPipeline(context, State{}, layout(0), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, nullptr, &hash); + Require(hash != 0, "a stored pipeline reported no store hash"); + std::uint64_t secondHash = 0; + auto second = CachedPipeline(context, State{}, layout(1), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, nullptr, &secondHash); + Require(secondHash != 0 && secondHash != hash, "two configurations reported the same store hash"); + touched = first; + other = second; + const auto hits = PipelineStoreTotals().hits; + Require(TouchCachedPipeline(context, hash, touched) == first && PipelineStoreTotals().hits == hits + 1, "touching a stored pipeline did not return it as a hit"); + Require(TouchCachedPipeline(context, hash, other) == nullptr, "touching a hash with another pipeline returned the stored one"); + Require(TouchCachedPipeline(context, hash, std::weak_ptr<Pipeline>{}) == nullptr, "touching a hash without a pipeline returned the stored one"); + } + for (std::uint32_t configuration = 2; configuration < PipelineStoreTotals().bound; ++configuration) static_cast<void>(CachedPipeline(context, State{}, layout(configuration), resources, shaders)); + Require(TouchCachedPipeline(context, hash, touched) != nullptr, "the first pipeline left a store that was not yet full"); + bool created = false; + static_cast<void>(CachedPipeline(context, State{}, layout(PipelineStoreTotals().bound), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &created)); + Require(created, "the configuration that fills the store was already stored"); + static_cast<void>(CachedPipeline(context, State{}, layout(0), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &created)); + Require(!created, "a touched pipeline was evicted as the least recently used one"); + static_cast<void>(CachedPipeline(context, State{}, layout(1), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &created)); + Require(created, "the least recently used pipeline stayed while a touched one was older"); + ClearCachedPipelines(context.device); + Require(TouchCachedPipeline(context, hash, touched) == nullptr, "touching a pipeline of a cleared store returned it"); + Require(mock.live == 0, "pipeline store touch tests leaked Vulkan objects"); +} + // A journal longer than the store's starting bound: 1032 recorded pipelines replayed into an empty // store all stay resident (the replay grows the bound instead of dropping what does not fit). void pipelineStoreReplayGrowthTests() { @@ -12467,6 +12518,11 @@ int main(int argc, char** argv) { std::cout << "pipeline store bound follows the evicted pipelines that come back\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-touch") { + pipelineStoreTouchTests(); + std::cout << "a touched pipeline is returned as a hit and stays recently used\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-lazy-buffer-pool") { pipelineStoreLazyBufferPoolTests(); std::cout << "cached pipelines survive lazy buffer pool creation\n"; From 91d601ecd0df37d9585fac91126c3fd26d2f21d9 Mon Sep 17 00:00:00 2001 From: Cesar Mora <augusto.morag@gmail.com> Date: Mon, 26 Oct 2026 22:12:08 -0600 Subject: [PATCH 681/702] fix(agc): guard a repeated storage surface over its whole range, as its lookup does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 51fd4ac377..774f8691cb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1550,9 +1550,10 @@ std::shared_ptr<StorageTexture> RepeatStorageSurface(const Context& context, con if (disabled || stored == nullptr) return nullptr; const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); - RequireNoMultisampleTarget(viewed.baseAddress, 1u, "a storage image access"); + const auto viewedBytes = DescribeSurface(viewed).guestBytes; + RequireNoMultisampleTarget(viewed.baseAddress, static_cast<std::size_t>(viewedBytes), "a storage image access"); if (DepthSurfaceStorage(context, {}, viewed, 0) != nullptr) return nullptr; - if (DepthPlanesStale()) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(DescribeSurface(viewed).guestBytes), DepthPlaneScope::Overlapped)); + if (DepthPlanesStale()) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(viewedBytes), DepthPlaneScope::Overlapped)); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; From 3181021a4f111cec3ea1c7f3ecb78d389987580b Mon Sep 17 00:00:00 2001 From: Cesar Mora <augusto.morag@gmail.com> Date: Mon, 26 Oct 2026 22:12:08 -0600 Subject: [PATCH 682/702] test(agc): a repeated storage surface refuses a guarded target past its first byte as its lookup does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 108 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 833ab62bb7..219ce701c3 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1846,6 +1846,8 @@ if(BUILD_TESTING) set_tests_properties(agc_draw_snapshot_epoch 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_repeat_storage_guard COMMAND agc_driver_recorder_tests --repeat-storage-guard-only) + set_tests_properties(agc_repeat_storage_guard PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) add_test(NAME agc_pending_write_snapshot COMMAND agc_driver_recorder_tests --pending-write-snapshot-only) set_tests_properties(agc_pending_write_snapshot PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index acec23f12a..3f371048c9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9326,6 +9326,109 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { std::cout << "Multisampled target guest access tests passed\n"; } +void repeatStorageGuardTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.NullDescriptors()) { + std::cout << "4x color targets or nullDescriptor unavailable: repeated storage surface guard not tested\n"; + return; + } + constexpr std::uint32_t side = 256; + constexpr std::uint32_t targetSide = 64; + constexpr std::size_t surfaceBytes = side * side * 4; + constexpr std::size_t targetBytes = 65536u; + constexpr std::size_t bytes = surfaceBytes + 2u * targetBytes; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the repeated storage surface block"); + std::memset(block, 0, bytes); + const auto address = reinterpret_cast<std::uint64_t>(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } unregister{base, block}; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + context.nullDescriptors = true; + TextureCache cache(context); + context.textureCache = &cache; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "the repeated storage surface has an unexpected size"); + const auto guard = [&](std::uint64_t at) { + ColorTarget target{}; + target.address = at; + target.surfaceAddress = at; + target.extent = {targetSide, targetSide}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.bytes = targetBytes; + target.samples = 4; + std::lock_guard gpu(GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the guarding multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + const auto clearTargets = [&] { + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + ClearMultisampleTargets(context.device); + }; + const auto refusal = [](const std::function<void()>& access) { + try { + access(); + } catch (const std::exception& error) { + return std::string(error.what()); + } + return std::string(); + }; + const auto stored = CachedStorageSurface(context, resource); + Require(stored != nullptr, "the storage surface has no image"); + Require(RepeatStorageSurface(context, resource, stored) == stored && CachedStorageSurface(context, resource) == stored, "without a guarded target the repeat path and the lookup returned different images"); + guard(address + surfaceBytes); + Require(RepeatStorageSurface(context, resource, stored) == stored && CachedStorageSurface(context, resource) == stored, "a guarded target that starts where the surface ends changed the repeat path's or the lookup's image"); + clearTargets(); + guard(address + targetBytes); + const auto repeated = refusal([&] { static_cast<void>(RepeatStorageSurface(context, resource, stored)); }); + const auto looked = refusal([&] { static_cast<void>(CachedStorageSurface(context, resource)); }); + Require(!looked.empty() && looked.find("a storage image access") != std::string::npos && looked.find("multisampled color target") != std::string::npos, "the lookup did not refuse a surface a guarded target overlaps after its first byte: " + looked); + Require(repeated == looked, "the repeat path decided differently from the lookup for a guarded target after the surface's first byte: " + repeated); + clearTargets(); + std::cout << "Repeated storage surface guard tests passed\n"; +} + void oneFragmentFmaskGuardTests(const Device& device) { const auto& base = device.GetContext(); if (!device.NullDescriptors()) { @@ -12127,6 +12230,10 @@ int main(int argc, char** argv) { std::cout << "Copied draw input tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--repeat-storage-guard-only") { + repeatStorageGuardTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") { multisampleGuestAccessTests(device, recorder); oneFragmentFmaskGuardTests(device); @@ -12191,6 +12298,7 @@ int main(int argc, char** argv) { bdaStagingTests(device, recorder); depthSurfaceProofTests(device, recorder); multisampleGuestAccessTests(device, recorder); + repeatStorageGuardTests(device, recorder); oneFragmentFmaskGuardTests(device); tiledResolveTests(device, recorder); fmaskDecompressMsaaTests(device, recorder); From fb4cc4e7cfefb525c85ff288495c57eda435f578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <adriadam10@gmail.com> Date: Wed, 28 Oct 2026 08:33:45 +0100 Subject: [PATCH 683/702] fix(agc): run agc_repeat_storage_guard with libc on the Windows PATH Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 219ce701c3..b36c6bd1b5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1868,7 +1868,7 @@ if(BUILD_TESTING) add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_repeat_storage_guard agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION From d8dfb7eab49ccea319e2acd3cb7d26c9bc56f611 Mon Sep 17 00:00:00 2001 From: tablefrogpillow <184222310+tablefrogpillow@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:47:39 +0000 Subject: [PATCH 684/702] fix(libkernel_sys): fail fpathconf on a kqueue or shm descriptor with EOPNOTSUPP _fpathconf_nid_postfix only treated FIFOs and sockets as descriptors without a vnode, so a kqueue or shm_open descriptor fell through to the UFS constants (LINK_MAX 32767, NAME_MAX 255, PIPE_BUF 512). FreeBSD 11.4 kern_fpathconf (kern_descrip.c:1411-1429) answers _PC_ASYNC_IO for every descriptor and returns EOPNOTSUPP for a no-vnode descriptor that is not a pipe or socket. Return EOPNOTSUPP for kqueue and shared-memory descriptors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- core/libs/prx/libkernel_sys/Export.cpp | 4 ++++ core/libs/tests/GuestKernelSys.cpp | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 55e4a22dff..21bfa947e9 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -34,6 +34,8 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" #ifdef _WIN32 #ifndef NOMINMAX @@ -74,6 +76,7 @@ constexpr int GuestEinval = 22; constexpr int GuestEexist = 17; constexpr int GuestExdev = 18; constexpr int GuestEdeadlk = 11; +constexpr int GuestEopnotsupp = 45; constexpr int GuestEagain = 35; constexpr int GuestEloop = 62; constexpr int GuestEnametoolong = 63; @@ -525,6 +528,7 @@ std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { #endif if (name == PcAsyncIo) return PosixAsynchronousIo; if (withoutVnode) return name == PcPipeBuf ? 512 : Fail(GuestEinval); + if (IsKqueueDescriptor(fd) || File::IsSharedMemoryDescriptor_nid_no_patch(fd)) return Fail(GuestEopnotsupp); return PathConfigurationValue(name); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 012c3ed514..f29b6eba97 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -861,6 +861,13 @@ void CheckKernelEnvironment() { Require(Kenv(dump, nullptr, nullptr, 0) == 0); } +template <typename F> +void CheckUnsupportedPathconf(F fn, int descriptor) { + Require(fn(descriptor, 53) == 200112); + for (const int name : {0, 1, 4, 5, 6, 7, 8, 12, 18, 54, 55, 61, -1}) + Require(fn(descriptor, name) == -1 && Error() == 45); +} + int main() { #ifdef _WIN32 auto hostDirectorySpelling = std::filesystem::current_path().native(); @@ -910,6 +917,17 @@ int main() { CheckDescriptorPathconf(_fpathconf_nid_postfix, descriptor); CheckPipePathconf(fpathconf_nid_postfix); CheckPipePathconf(_fpathconf_nid_postfix); + const int kqueueDescriptor = kqueue_nid_postfix(); + Require(kqueueDescriptor >= 0); + CheckUnsupportedPathconf(fpathconf_nid_postfix, kqueueDescriptor); + CheckUnsupportedPathconf(_fpathconf_nid_postfix, kqueueDescriptor); + Require(close_nid_postfix(kqueueDescriptor) == 0); + const char* const anonymousSharedMemory = reinterpret_cast<const char*>(1); + const int sharedMemoryDescriptor = shm_open_nid_postfix(anonymousSharedMemory, 0x100202, 0600); + Require(sharedMemoryDescriptor >= 0); + CheckUnsupportedPathconf(fpathconf_nid_postfix, sharedMemoryDescriptor); + CheckUnsupportedPathconf(_fpathconf_nid_postfix, sharedMemoryDescriptor); + Require(close_nid_postfix(sharedMemoryDescriptor) == 0); Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, reinterpret_cast<KernelStatfs*>(viaPublic)) == -1 && Error() == 9); Require(fstat_nid_postfix(descriptor, &viaFstat) == -1 && Error() == 9); From 90d0daedc9ec0cbeba00749ea41a869354c97eee Mon Sep 17 00:00:00 2001 From: tablefrogpillow <184222310+tablefrogpillow@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:14:47 +0000 Subject: [PATCH 685/702] test(libkernel_sys): use a named shm descriptor in the fpathconf check An anonymous shm descriptor is recognised as shared memory only on Linux, where the memfd link name is matched; on Windows the anonymous %TEMP% object is not registered in the shared-descriptor table, so the descriptor is not recognised and fpathconf never reaches the EOPNOTSUPP branch. A named descriptor is recognised on both platforms, so the check now opens a named object and unlinks it, keeping the same branch coverage without a Linux-only assumption. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- core/libs/tests/GuestKernelSys.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index f29b6eba97..7be53bbd75 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -922,12 +922,14 @@ int main() { CheckUnsupportedPathconf(fpathconf_nid_postfix, kqueueDescriptor); CheckUnsupportedPathconf(_fpathconf_nid_postfix, kqueueDescriptor); Require(close_nid_postfix(kqueueDescriptor) == 0); - const char* const anonymousSharedMemory = reinterpret_cast<const char*>(1); - const int sharedMemoryDescriptor = shm_open_nid_postfix(anonymousSharedMemory, 0x100202, 0600); + const std::string namedSharedMemory = "/anyps5-kernel-sys-pathconf-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int sharedMemoryDescriptor = shm_open_nid_postfix(namedSharedMemory.c_str(), SCE_KERNEL_O_RDWR | SCE_KERNEL_O_CREAT, 0600); Require(sharedMemoryDescriptor >= 0); CheckUnsupportedPathconf(fpathconf_nid_postfix, sharedMemoryDescriptor); CheckUnsupportedPathconf(_fpathconf_nid_postfix, sharedMemoryDescriptor); Require(close_nid_postfix(sharedMemoryDescriptor) == 0); + Require(shm_unlink_nid_postfix(namedSharedMemory.c_str()) == 0); Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, reinterpret_cast<KernelStatfs*>(viaPublic)) == -1 && Error() == 9); Require(fstat_nid_postfix(descriptor, &viaFstat) == -1 && Error() == 9); From f87b6289ac15071a52c51cf450b21ed48777348f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <adriadam10@gmail.com> Date: Wed, 28 Oct 2026 08:17:01 +0100 Subject: [PATCH 686/702] fix(libkernel): export the kqueue descriptor check to libkernel_sys Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/Equeue/Equeue.hpp | 2 +- core/libs/prx/libkernel/Equeue/src/FileEvent.cpp | 2 +- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 2 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 10 +++++----- core/libs/prx/libkernel/Socket/src/Socket.cpp | 2 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 +- core/libs/prx/libkernel_sys/Export.cpp | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 266bb4a24e..d11186ef37 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -131,7 +131,7 @@ void EqueueDescriptorClosed(int descriptor); void KqueueDescriptorClosed(int descriptor); void EqueueDescriptorReadShut(int descriptor); void KqueueDescriptorReadShut(int descriptor); -bool IsKqueueDescriptor(int descriptor); +bool IsKqueueDescriptor_nid_no_patch(int descriptor); std::optional<bool> KqueueReadable(int descriptor); KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t flags, void* udata); void RequireVnodeNotes(uint32_t fflags); diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index a0b5e49fd3..a9126e1e7e 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -695,7 +695,7 @@ int VnodeDescriptorError(int descriptor) { if (!DescriptorIsOpen_nid_no_patch(descriptor)) { return GUEST_EBADF; } - return IsKqueueDescriptor(descriptor) ? GUEST_EINVAL : 0; + return IsKqueueDescriptor_nid_no_patch(descriptor) ? GUEST_EINVAL : 0; } } diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 38ae2f53a6..eea3d985ab 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -511,7 +511,7 @@ std::optional<std::uint64_t> ScanDeadline(const KernelTimespec& timeout) { } -bool IsKqueueDescriptor(int descriptor) { +bool IsKqueueDescriptor_nid_no_patch(int descriptor) { return static_cast<bool>(FindKqueue(descriptor)); } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index fbecc93658..47a65d543e 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -490,7 +490,7 @@ static bool IsSeekableDescriptor(int descriptor) { } static int PositionedArgumentError(int descriptor, bool write, bool nullBuffer, std::int64_t offset) { - if (IsKqueueDescriptor(descriptor)) return GUEST_ESPIPE; + if (IsKqueueDescriptor_nid_no_patch(descriptor)) return GUEST_ESPIPE; if (!nullBuffer && offset >= 0) return 0; if (!GuestSockets::IsOpen(descriptor) && !DescriptorIsOpen_nid_no_patch(descriptor)) return GUEST_EBADF; if (!IsSeekableDescriptor(descriptor)) return GUEST_ESPIPE; @@ -772,7 +772,7 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { int APS5_VABI fchflags_nid_postfix(int d, std::uint64_t flags) { if (File::IsVirtualDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); - if (GuestSockets::IsOpen(d) || IsKqueueDescriptor(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); + if (GuestSockets::IsOpen(d) || IsKqueueDescriptor_nid_no_patch(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); File::RefuseArchivedFile(d, "fchflags"); if (!File::IsVnode(d)) { if (!DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); @@ -848,7 +848,7 @@ extern "C" { int APS5_VABI dup_nid_postfix(int d) { if (GuestSockets::IsOpen(d)) return GuestSockets::Duplicate(d); if (d < 0) return PosixFailure(GUEST_EBADF); - if (IsKqueueDescriptor(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); + if (IsKqueueDescriptor_nid_no_patch(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); #ifdef _WIN32 const int duplicate = WithoutParameterHandler([d] { return ::_dup(d); }); if (duplicate >= 0) File::DuplicateSharedMemoryDescriptor_nid_no_patch(d, duplicate); @@ -879,7 +879,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { return to; } const bool sourceIsRandom = File::IsRandomDevice(from); - if (from != to && IsKqueueDescriptor(from)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); + if (from != to && IsKqueueDescriptor_nid_no_patch(from)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); #ifdef _WIN32 if (WithoutParameterHandler([from] { return ::_get_osfhandle(from) == -1 ? -1 : 0; }) != 0) return PosixFailure(GUEST_EBADF); if (from == to) return to; @@ -959,7 +959,7 @@ int APS5_VABI flock_nid_postfix(int d, int operation) { int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { if (File::IsVirtualDirectoryDescriptor(d)) return PosixFailure(GUEST_EOPNOTSUPP); - if (GuestSockets::IsOpen(d) || IsKqueueDescriptor(d)) return PosixFailure(GUEST_EINVAL); + if (GuestSockets::IsOpen(d) || IsKqueueDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); if (d < 0) return PosixFailure(GUEST_EBADF); File::RefuseArchivedFile(d, "fstatfs"); KernelStatfs result{}; diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 399e530a79..58d350bf50 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -210,7 +210,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* kernel.entries.push_back({descriptor, events, 0}); kernel.slots.push_back(requested.size()); requested.push_back({descriptor, events, 0}); - } else if (IsKqueueDescriptor(descriptor)) { + } else if (IsKqueueDescriptor_nid_no_patch(descriptor)) { kqueues.entries.push_back({descriptor, events, 0}); kqueues.slots.push_back(requested.size()); requested.push_back({descriptor, events, 0}); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 1131f91a2a..de7b3a11d1 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1869,7 +1869,7 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c ++ready; continue; } - if (IsKqueueDescriptor(entry.descriptor)) { + if (IsKqueueDescriptor_nid_no_patch(entry.descriptor)) { kqueues.push_back(i); continue; } diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 21bfa947e9..61c85af33a 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -528,7 +528,7 @@ std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { #endif if (name == PcAsyncIo) return PosixAsynchronousIo; if (withoutVnode) return name == PcPipeBuf ? 512 : Fail(GuestEinval); - if (IsKqueueDescriptor(fd) || File::IsSharedMemoryDescriptor_nid_no_patch(fd)) return Fail(GuestEopnotsupp); + if (IsKqueueDescriptor_nid_no_patch(fd) || File::IsSharedMemoryDescriptor_nid_no_patch(fd)) return Fail(GuestEopnotsupp); return PathConfigurationValue(name); } From 149ec09adf336dd424dede4f8a81377ca976a377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <adriadam10@gmail.com> Date: Wed, 28 Oct 2026 08:27:06 +0100 Subject: [PATCH 687/702] fix(libkernel): give the descriptor checks C linkage so libkernel_sys can import them Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/Equeue/Equeue.hpp | 2 +- core/libs/prx/libkernel/File/include/SharedMemory.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index d11186ef37..4cace734bd 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -131,7 +131,7 @@ void EqueueDescriptorClosed(int descriptor); void KqueueDescriptorClosed(int descriptor); void EqueueDescriptorReadShut(int descriptor); void KqueueDescriptorReadShut(int descriptor); -bool IsKqueueDescriptor_nid_no_patch(int descriptor); +extern "C" bool IsKqueueDescriptor_nid_no_patch(int descriptor); std::optional<bool> KqueueReadable(int descriptor); KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t flags, void* udata); void RequireVnodeNotes(uint32_t fflags); diff --git a/core/libs/prx/libkernel/File/include/SharedMemory.hpp b/core/libs/prx/libkernel/File/include/SharedMemory.hpp index 7d9be97385..8890cc4ab4 100644 --- a/core/libs/prx/libkernel/File/include/SharedMemory.hpp +++ b/core/libs/prx/libkernel/File/include/SharedMemory.hpp @@ -9,7 +9,7 @@ namespace File { -bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor); +extern "C" bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor); std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size); std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset); std::int64_t SeekSharedMemory_nid_no_patch(int descriptor, std::int64_t offset, int whence); From 944192d89dd3a1608c9504cc8853a4b3994b58ed Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:44:31 +0000 Subject: [PATCH 688/702] fix(libSceNpTrophy): keep a trophy locked when its unlock state cannot be saved sceNpTrophyUnlockTrophy inserted the id into the in-memory set and then saved the state file. A save that threw (the state directory or the rename failed) left the trophy unlocked in memory but not on disk, so a retry answered TROPHY_ALREADY_UNLOCKED and the unlock was lost at the next start. The new set is now built on a copy, written, and only then installed. SaveState also never looked at the stream after writing, so a failed write (for example a full disk) was ignored and the truncated temporary file was renamed over the good state file. It now flushes, checks the stream, removes the partial temporary file and throws. --- core/libs/prx/libSceNpTrophy/Export.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpTrophy/Export.cpp b/core/libs/prx/libSceNpTrophy/Export.cpp index 2571c199f6..c5cdd826a1 100644 --- a/core/libs/prx/libSceNpTrophy/Export.cpp +++ b/core/libs/prx/libSceNpTrophy/Export.cpp @@ -1,3 +1,4 @@ +#include <stdexcept> #include "prx/libSceNpTrophy/include/Api.hpp" #include <array> #include <cstdint> @@ -107,12 +108,19 @@ void LoadState(Context& context) { } } -void SaveState(const Context& context) { +void SaveState(const Context& context, const std::set<int>& unlocked) { std::filesystem::create_directories(context.state.parent_path()); const auto temporary = context.state.string() + ".tmp"; { std::ofstream file(temporary, std::ios::trunc); - for (const auto id : context.unlocked) file << id << '\n'; + for (const auto id : unlocked) file << id << '\n'; + file.flush(); + if (!file) { + file.close(); + std::error_code removeError; + std::filesystem::remove(temporary, removeError); + throw std::runtime_error("sceNpTrophy: cannot write the unlock state " + temporary); + } } std::filesystem::rename(temporary, context.state); } @@ -251,8 +259,10 @@ int APS5_VABI sceNpTrophyUnlockTrophy(int contextId, int handle, int trophyId, i if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; if (context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; - context->unlocked.insert(trophyId); - SaveState(*context); + auto updated = context->unlocked; + updated.insert(trophyId); + SaveState(*context, updated); + context->unlocked = std::move(updated); *platinumId = NP_TROPHY_INVALID_ID; return 0; } From 161c9d55c68d8333073b651ffbfeabb832b8ab23 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:44:31 +0000 Subject: [PATCH 689/702] test(libSceNpTrophy): fail the state write of an unlock and unlock again A directory in place of the temporary state file makes the save fail; the unlock has to report it, leave trophy 0 locked, and succeed once the directory is gone. --- core/libs/tests/GuestNpTrophy.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/libs/tests/GuestNpTrophy.cpp b/core/libs/tests/GuestNpTrophy.cpp index 768dd77112..2564fbc361 100644 --- a/core/libs/tests/GuestNpTrophy.cpp +++ b/core/libs/tests/GuestNpTrophy.cpp @@ -97,6 +97,18 @@ int main() { } Require(saved == 1, "unlock state saved for the user"); + std::filesystem::create_directories(root / "_trophy/trophy00.1.tmp"); + bool saveFailed = false; + try { + sceNpTrophyUnlockTrophy(context, handle, 0, &platinum); + } catch (const std::exception&) { + saveFailed = true; + } + std::filesystem::remove_all(root / "_trophy/trophy00.1.tmp"); + Require(saveFailed, "an unlock whose state cannot be written reports the failure"); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, reinterpret_cast<TrophyFlagArray*>(flags), &count) == 0 && flags[0] == 2u, "a failed save leaves the trophy locked"); + Require(sceNpTrophyUnlockTrophy(context, handle, 0, &platinum) == 0, "the unlock succeeds once the state can be written"); + Require(sceNpTrophyCreateContext(&missing, 1, 1, 0) == 0, "context for a label without a package"); Require(sceNpTrophyRegisterContext(missing, handle, 0) == TitleConfNotInstalled, "register without a trophy package"); Require(sceNpTrophyCreateHandle(nullptr) == InvalidArgument, "null handle creation"); From 475484a00f8fc2f0c635e0ed9efdf87c35d9325d Mon Sep 17 00:00:00 2001 From: Qu Yi <quyimail@foxmail.com> Date: Wed, 28 Oct 2026 15:52:57 +0800 Subject: [PATCH 690/702] feat(libSceIcu): implement C string conversion helpers --- core/libs/prx/libSceIcu/CMakeLists.txt | 14 ++ core/libs/prx/libSceIcu/CString.cpp | 68 ++++++++++ core/libs/prx/libSceIcu/CString.hpp | 11 ++ core/libs/prx/libSceIcu/Export.cpp | 25 ---- core/libs/prx/libSceIcu/libSceIcu.hpp | 1 + core/libs/tests/GuestIcuCString.cpp | 177 +++++++++++++++++++++++++ docs/dev/debt/libSceIcu.md | 4 +- 7 files changed, 274 insertions(+), 26 deletions(-) create mode 100644 core/libs/prx/libSceIcu/CString.cpp create mode 100644 core/libs/prx/libSceIcu/CString.hpp create mode 100644 core/libs/tests/GuestIcuCString.cpp diff --git a/core/libs/prx/libSceIcu/CMakeLists.txt b/core/libs/prx/libSceIcu/CMakeLists.txt index 14fb9a0761..f7f80b1cae 100644 --- a/core/libs/prx/libSceIcu/CMakeLists.txt +++ b/core/libs/prx/libSceIcu/CMakeLists.txt @@ -1,6 +1,20 @@ add_library(libSceIcu SHARED EXCLUDE_FROM_ALL Export.cpp Invariant.cpp Version.cpp Unescape.cpp) target_include_directories(libSceIcu PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceIcu) +target_sources(libSceIcu PRIVATE CString.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_icu_cstring_tests ${LIBS_INCLUDE_DIR}/tests/GuestIcuCString.cpp) + target_include_directories(guest_icu_cstring_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_icu_cstring_tests PRIVATE libSceIcu libc) + configure_windows_unwind(guest_icu_cstring_tests) + add_test(NAME guest_icu_cstring COMMAND guest_icu_cstring_tests) + set_tests_properties(guest_icu_cstring PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_icu_cstring PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceIcu>") + endif() +endif() if(BUILD_TESTING) add_test_executable(guest_icu_version_tests ${LIBS_INCLUDE_DIR}/tests/GuestIcuVersion.cpp) diff --git a/core/libs/prx/libSceIcu/CString.cpp b/core/libs/prx/libSceIcu/CString.cpp new file mode 100644 index 0000000000..386e443fc8 --- /dev/null +++ b/core/libs/prx/libSceIcu/CString.cpp @@ -0,0 +1,68 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" +#include <algorithm> +#include <bit> +#include <cstdint> +#include <type_traits> + +namespace { + +template<class TValue> +std::int32_t FormatInteger(char* buffer, TValue value, std::uint32_t radix) { + using TUnsigned = std::make_unsigned_t<TValue>; + auto magnitude = static_cast<TUnsigned>(value); + const bool negative = value < 0 && radix == 10; + std::int32_t length = 0; + if (negative) { + magnitude = TUnsigned{0} - magnitude; + buffer[length++] = '-'; + } + const std::int32_t firstDigit = length; + do { + buffer[length++] = "0123456789ABCDEF"[magnitude % radix]; + magnitude /= radix; + } while (magnitude); + std::reverse(buffer + firstDigit, buffer + length); + buffer[length] = 0; + return length; +} + +char* ConvertCase(char* text, char first, char last, int offset) { + if (!text) return nullptr; + for (char* cursor = text;; ++cursor) { + const char value = *cursor; + *cursor = value >= first && value <= last ? static_cast<char>(value + offset) : value; + if (!value) return text; + } +} + +} + +extern "C" { + +std::int32_t APS5_VABI T_CString_integerToString_nid_postfix(char* buffer, std::int32_t value, std::int32_t radix) { + if (!buffer || radix < 2 || radix > 16) APS5_INVALID_ARG_EX; + return FormatInteger(buffer, value, static_cast<std::uint32_t>(radix)); +} + +std::int32_t APS5_VABI T_CString_int64ToString_nid_postfix(char* buffer, std::int64_t value, std::uint32_t radix) { + if (!buffer || radix < 2 || radix > 16) APS5_INVALID_ARG_EX; + return FormatInteger(buffer, value, radix); +} + +std::int32_t APS5_VABI T_CString_stringToInteger_nid_postfix(const char* text, std::int32_t radix) { + if (!text) APS5_INVALID_ARG_EX; + const auto value = static_cast<std::uint32_t>(strtoul_nid_postfix(text, nullptr, radix)); + return std::bit_cast<std::int32_t>(value); +} + +char* APS5_VABI T_CString_toLowerCase_nid_postfix(char* text) { + return ConvertCase(text, 'A', 'Z', 'a' - 'A'); +} + +char* APS5_VABI T_CString_toUpperCase_nid_postfix(char* text) { + return ConvertCase(text, 'a', 'z', 'A' - 'a'); +} + +} diff --git a/core/libs/prx/libSceIcu/CString.hpp b/core/libs/prx/libSceIcu/CString.hpp new file mode 100644 index 0000000000..ce1984aaae --- /dev/null +++ b/core/libs/prx/libSceIcu/CString.hpp @@ -0,0 +1,11 @@ +#pragma once +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> + +extern "C" { +std::int32_t APS5_VABI T_CString_integerToString_nid_postfix(char* buffer, std::int32_t value, std::int32_t radix); +std::int32_t APS5_VABI T_CString_int64ToString_nid_postfix(char* buffer, std::int64_t value, std::uint32_t radix); +std::int32_t APS5_VABI T_CString_stringToInteger_nid_postfix(const char* text, std::int32_t radix); +char* APS5_VABI T_CString_toLowerCase_nid_postfix(char* text); +char* APS5_VABI T_CString_toUpperCase_nid_postfix(char* text); +} diff --git a/core/libs/prx/libSceIcu/Export.cpp b/core/libs/prx/libSceIcu/Export.cpp index cef6411bce..1b37f59c57 100644 --- a/core/libs/prx/libSceIcu/Export.cpp +++ b/core/libs/prx/libSceIcu/Export.cpp @@ -6,31 +6,6 @@ extern "C" { -int APS5_VABI sceIcuUnknown_2m4vg20UoXI_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("2m4vg20UoXI", sceIcuUnknown_2m4vg20UoXI_nid_no_patch); - -int APS5_VABI sceIcuUnknown_wwOr3NA7FRc_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("wwOr3NA7FRc", sceIcuUnknown_wwOr3NA7FRc_nid_no_patch); - -int APS5_VABI sceIcuUnknown_RGVOIIycuyA_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("RGVOIIycuyA", sceIcuUnknown_RGVOIIycuyA_nid_no_patch); - -int APS5_VABI sceIcuUnknown_9duJwhiYGCE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("9duJwhiYGCE", sceIcuUnknown_9duJwhiYGCE_nid_no_patch); - -int APS5_VABI sceIcuUnknown_wuiJZ6Iy6Mc_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("wuiJZ6Iy6Mc", sceIcuUnknown_wuiJZ6Iy6Mc_nid_no_patch); - int APS5_VABI sceIcuUnknown_VZFw_xSe6Mo_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceIcu/libSceIcu.hpp b/core/libs/prx/libSceIcu/libSceIcu.hpp index d8c66857ef..eb7f63dc9c 100644 --- a/core/libs/prx/libSceIcu/libSceIcu.hpp +++ b/core/libs/prx/libSceIcu/libSceIcu.hpp @@ -1,4 +1,5 @@ #pragma once #include "Invariant.hpp" #include "Version.hpp" +#include "CString.hpp" #include "Unescape.hpp" diff --git a/core/libs/tests/GuestIcuCString.cpp b/core/libs/tests/GuestIcuCString.cpp new file mode 100644 index 0000000000..a2e38d77dc --- /dev/null +++ b/core/libs/tests/GuestIcuCString.cpp @@ -0,0 +1,177 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include "prx/libc/include/Api.hpp" +#include <algorithm> +#include <array> +#include <bit> +#include <charconv> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <limits> +#include <stdexcept> +#include <string> +#include <string_view> +#include <type_traits> + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "ICU C string: %s\n", message); + std::abort(); +} + +template<class TCall> +bool Rejects(TCall call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +template<class TValue> +void CheckFormat(TValue value, int radix) { + std::array<char, 80> expected{}; + const auto converted = radix == 10 + ? std::to_chars(expected.data(), expected.data() + expected.size(), value, radix) + : std::to_chars(expected.data(), expected.data() + expected.size(), static_cast<std::make_unsigned_t<TValue>>(value), radix); + Require(converted.ec == std::errc{}, "standard formatting oracle succeeds"); + for (char* cursor = expected.data(); cursor != converted.ptr; ++cursor) + if (*cursor >= 'a' && *cursor <= 'f') *cursor = static_cast<char>(*cursor - ('a' - 'A')); + const std::string_view wanted(expected.data(), converted.ptr - expected.data()); + std::array<char, 80> actual; + actual.fill('@'); + std::int32_t length; + if constexpr (sizeof(TValue) == 4) length = T_CString_integerToString_nid_postfix(actual.data() + 1, value, radix); + else length = T_CString_int64ToString_nid_postfix(actual.data() + 1, value, static_cast<std::uint32_t>(radix)); + Require(length == static_cast<std::int32_t>(wanted.size()), "format length excludes the terminator"); + Require(std::string_view(actual.data() + 1, wanted.size()) == wanted, "format matches independent standard oracle"); + Require(actual[1 + wanted.size()] == 0, "format terminates the string"); + Require(actual.front() == '@', "format preserves the prefix canary"); + Require(std::all_of(actual.begin() + wanted.size() + 2, actual.end(), [](char value) { return value == '@'; }), "format preserves the suffix canary"); +} + +void Formatting() { + constexpr std::array<std::int32_t, 9> values32{0, 1, -1, 15, -15, 431, -431, INT32_MIN, INT32_MAX}; + constexpr std::array<std::int64_t, 11> values64{0, 1, -1, 15, -15, 431, -431, INT32_MIN, INT32_MAX, INT64_MIN, INT64_MAX}; + for (int radix = 2; radix <= 16; ++radix) { + for (auto value : values32) CheckFormat(value, radix); + for (auto value : values64) CheckFormat(value, radix); + } + std::uint64_t random = 0x18cb3a2ef4796d05; + for (int sample = 0; sample < 1024; ++sample) { + random ^= random << 13; + random ^= random >> 7; + random ^= random << 17; + for (int radix = 2; radix <= 16; ++radix) { + CheckFormat(std::bit_cast<std::int32_t>(static_cast<std::uint32_t>(random)), radix); + CheckFormat(std::bit_cast<std::int64_t>(random), radix); + } + } +} + +void InvalidFormatting() { + std::array<char, 80> buffer; + buffer.fill('@'); + for (int radix : {-1, 0, 1, 17, 36, INT32_MAX}) { + Require(Rejects([&] { T_CString_integerToString_nid_postfix(buffer.data(), 42, radix); }), "invalid 32-bit format radix throws"); + Require(Rejects([&] { T_CString_int64ToString_nid_postfix(buffer.data(), 42, static_cast<std::uint32_t>(radix)); }), "invalid 64-bit format radix throws"); + Require(std::all_of(buffer.begin(), buffer.end(), [](char value) { return value == '@'; }), "invalid radix leaves the buffer unchanged"); + } + Require(Rejects([] { T_CString_integerToString_nid_postfix(nullptr, 42, 10); }), "null 32-bit format buffer throws"); + Require(Rejects([] { T_CString_int64ToString_nid_postfix(nullptr, 42, 10); }), "null 64-bit format buffer throws"); +} + +void Parsing() { + struct Case { + const char* text; + int radix; + std::int32_t value; + int error; + }; + constexpr int SavedError = 1234; + constexpr Case cases[] = { + {"", 10, 0, SavedError}, + {"words", 10, 0, SavedError}, + {" +", 10, 0, SavedError}, + {"0", 10, 0, SavedError}, + {" \t\n\r\f\v-42tail", 10, -42, SavedError}, + {"+42", 10, 42, SavedError}, + {"2147483647", 10, INT32_MAX, SavedError}, + {"2147483648", 10, INT32_MIN, SavedError}, + {"-2147483648", 10, INT32_MIN, SavedError}, + {"4294967295", 10, -1, SavedError}, + {"4294967296", 10, 0, SavedError}, + {"4294967297", 10, 1, SavedError}, + {"-4294967297", 10, -1, SavedError}, + {"9223372036854775808", 10, 0, SavedError}, + {"18446744073709551615", 10, -1, SavedError}, + {"-18446744073709551615", 10, 1, SavedError}, + {"18446744073709551616", 10, -1, 34}, + {"-18446744073709551616", 10, -1, 34}, + {"999999999999999999999999999999", 10, -1, 34}, + {"101101tail", 2, 45, SavedError}, + {"0b11", 0, 0, SavedError}, + {"0b11", 2, 0, SavedError}, + {"077", 0, 63, SavedError}, + {"077", 10, 77, SavedError}, + {"0x2A", 0, 42, SavedError}, + {"0X2a", 16, 42, SavedError}, + {"0x", 16, 0, SavedError}, + {"FFFFFFFF", 16, -1, SavedError}, + {"100000001", 16, 1, SavedError}, + {"zZ", 36, 1295, SavedError}, + {"42", 1, 0, 22}, + {"42", 37, 0, 22}, + {"42", -1, 0, 22}, + }; + for (const auto& test : cases) { + *__error_nid_postfix() = SavedError; + const auto actual = T_CString_stringToInteger_nid_postfix(test.text, test.radix); + const int error = *__error_nid_postfix(); + Require(actual == test.value, "parser preserves LP64 strtoul results before narrowing"); + Require(error == test.error, "parser preserves strtoul error behavior"); + } + for (int radix = 2; radix <= 36; ++radix) { + for (std::uint64_t value : std::array<std::uint64_t, 8>{0, 1, 35, 431, UINT32_MAX, std::uint64_t{UINT32_MAX} + 1, INT64_MAX, UINT64_MAX}) { + std::array<char, 80> text{}; + const auto converted = std::to_chars(text.data(), text.data() + text.size() - 1, value, radix); + Require(converted.ec == std::errc{}, "standard parse input generation succeeds"); + const auto expected = std::bit_cast<std::int32_t>(static_cast<std::uint32_t>(value)); + Require(T_CString_stringToInteger_nid_postfix(text.data(), radix) == expected, "parser accepts every supported radix"); + } + } + Require(Rejects([] { T_CString_stringToInteger_nid_postfix(nullptr, 10); }), "null parse source throws"); +} + +void CaseConversion() { + Require(T_CString_toLowerCase_nid_postfix(nullptr) == nullptr, "null lowercase input returns null"); + Require(T_CString_toUpperCase_nid_postfix(nullptr) == nullptr, "null uppercase input returns null"); + for (int value = 0; value <= 255; ++value) { + const char input = static_cast<char>(value); + std::array<char, 4> lower{input, 0, 'A', 'Z'}; + std::array<char, 4> upper{input, 0, 'a', 'z'}; + Require(T_CString_toLowerCase_nid_postfix(lower.data()) == lower.data(), "lowercase returns the original pointer"); + Require(T_CString_toUpperCase_nid_postfix(upper.data()) == upper.data(), "uppercase returns the original pointer"); + Require(lower[0] == (value >= 65 && value <= 90 ? static_cast<char>(value + 32) : input), "only ASCII uppercase letters become lowercase"); + Require(upper[0] == (value >= 97 && value <= 122 ? static_cast<char>(value - 32) : input), "only ASCII lowercase letters become uppercase"); + Require(lower[1] == 0 && lower[2] == 'A' && lower[3] == 'Z', "lowercase stops at the terminator"); + Require(upper[1] == 0 && upper[2] == 'a' && upper[3] == 'z', "uppercase stops at the terminator"); + } + std::string mixed = "aBcD-zZ [\\]^_` @09\tI-i"; + T_CString_toLowerCase_nid_postfix(mixed.data()); + Require(mixed == "abcd-zz [\\]^_` @09\ti-i", "lowercase traverses complete strings"); + T_CString_toUpperCase_nid_postfix(mixed.data()); + Require(mixed == "ABCD-ZZ [\\]^_` @09\tI-I", "uppercase traverses complete strings"); +} + +} + +int main() { + Formatting(); + InvalidFormatting(); + Parsing(); + CaseConversion(); +} diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index f38e916c26..c9e6d55d7a 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -6,10 +6,12 @@ ### Functional +- [C string helpers](../../../core/libs/prx/libSceIcu/CString.cpp) have no console ABI or behavior verification. Their names/NIDs are in the PS5 3.20 inventory; signatures and behavior follow ICU 78.1's [cstring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/cstring.cpp), unchanged since 65.1 apart from casts. Formatting accepts radices 2–16; null buffers or other radices throw an invalid-argument exception, whereas ICU dereferences the buffer and only asserts the radix. Signed minima and outputs exceeding ICU's 30-byte temporary use unsigned arithmetic and caller storage without reproducing upstream undefined behavior. Parsing uses guest LP64 `strtoul` and keeps the low 32 bits; a null source throws. Cross-firmware availability, native invalid-input behavior and the undefined upstream cases remain unverified. + - [Escape decoding](../../../core/libs/prx/libSceIcu/Unescape.cpp) has no behavior or ABI verification on console hardware. It follows ICU 78.1's [ustring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/ustring.cpp), including rejection of eight-digit braced hexadecimal escapes although its public header advertises up to eight digits. Null source or offset pointers, null callbacks with an in-range offset, negative capacity with a nonnull destination and source lengths exceeding `INT32_MAX` throw. Null-destination preflight ignores capacity. Closes when: compared with the console, including callback ABI, byte conversion, capacity limits and braced hexadecimal parsing. - [Invariant character and version helpers](../../../core/libs/prx/libSceIcu/Version.cpp) are unverified on a console. `u_charsToUChars`, `u_UCharsToChars`, `u_versionFromString`, `u_versionFromUString` and `u_versionToString` follow ICU 78.1 [uinvchar.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/uinvchar.cpp) and [putil.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/putil.cpp), unchanged since 65.1. Firmware 13.42 reports ICU 78.1 and exports none of them ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6400470698)); the [3.20 table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) has all five. Version parts use the guest `strtoul`, truncated to eight bits; `u_versionToString` keeps ICU's dropped zero tens digit (105 becomes `15`). A null buffer with a positive conversion length throws; ICU dereferences it. Closes when: compared with the console. ### Known export names; ABI prototypes unverified -- The 112 `sceIcuUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceIcu/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 107 `sceIcuUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceIcu/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From 2b03bf743adf7f008e75b2f29f854eabac8a9476 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Wed, 28 Oct 2026 02:55:34 -0500 Subject: [PATCH 691/702] docs(debt): drop libSceMbus's stale protected-body note ab4a949f07 ended the "sceMbus remaining declarations" entry with "All protected stub bodies remain unchanged." It was written on top of afbd9b882 (#8305), which had already made sceMbusAcquireControlList return 0x809B00FF and sceMbusTerm return 0, and the same entry says both return constants. Drop the sentence. --- docs/dev/debt/libSceMbus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index 9078d4ebc6..4fccfba7a6 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -8,7 +8,7 @@ - [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. -- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): SetDeviceFunctionState takes a 64-bit device identifier and a 180-byte aggregate by value on the stack. Its service-side fields and source alignment remain unresolved; the declaration preserves the observed stack ABI with opaque storage. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. All protected stub bodies remain unchanged. +- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): SetDeviceFunctionState takes a 64-bit device identifier and a 180-byte aggregate by value on the stack. Its service-side fields and source alignment remain unresolved; the declaration preserves the observed stack ABI with opaque storage. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. - [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. From 051b3e017b687bce396bd1b000e89c2428e990b3 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Fri, 23 Oct 2026 21:01:21 +0400 Subject: [PATCH 692/702] fix(agc): accept compute queue 0x58 on SubmitAcb A PS5 measurement already recorded in the debt note shows queue 0x58 succeeds; raise the exclusive SubmissionQueueEnd and cover it in agc_driver_tests. --- .../Execution/include/Driver/Queues/SubmissionRing.hpp | 2 +- .../libSceAgcDriver/Execution/src/SubmissionCapture.cpp | 3 ++- core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp | 5 +++-- .../libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp | 5 +++-- core/libs/prx/libSceAgcDriver/tests/Submit.cpp | 8 +++++--- docs/dev/debt/libSceAgcDriver.md | 1 + 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp index a29def6cff..5d19421c5a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp @@ -9,7 +9,7 @@ namespace AgcDriver::DriverDetail { -inline constexpr std::uint32_t SubmissionQueueEnd = 0x58; +inline constexpr std::uint32_t SubmissionQueueEnd = 0x59; std::size_t SubmissionRingCapacity(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp index 9caddde7bd..606d6e0554 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include <algorithm> #include <bit> #include <charconv> @@ -174,7 +175,7 @@ std::string_view SubmissionCapture::KindName(SubmissionKind kind) { std::string_view SubmissionCapture::QueueType(std::uint32_t queue) { if (queue == 0) return "graphics"; - if (queue >= 0x20 && queue < 0x58) return "compute"; + if (queue >= 0x20 && queue < DriverDetail::SubmissionQueueEnd) return "compute"; char what[64]; std::snprintf(what, sizeof(what), "queue 0x%x is neither the graphics queue nor a compute queue", queue); throw std::runtime_error(std::string("submission capture: ") + what); diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp index 5f9dff1e4c..c6bda0c24a 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include <cstdint> #include <cstddef> @@ -10,7 +11,7 @@ extern "C" { int APS5_VABI sceAgcDriverSubmitAcb(uint32_t queue, const Packet* packet) { - if (queue < 0x20 || queue >= 0x58) { + if (queue < 0x20 || queue >= AgcDriver::DriverDetail::SubmissionQueueEnd) { throw std::runtime_error(std::string(__func__) + ": unsupported compute queue"); } AgcDriver::Submit(packet, queue); @@ -19,7 +20,7 @@ int APS5_VABI sceAgcDriverSubmitAcb(uint32_t queue, const Packet* packet) { int APS5_VABI sceAgcDriverSubmitMultiAcbs(uint32_t queue, uint32_t* const* acbs, const uint32_t* sizes_in_dwords, uint32_t count) { if (count == 0) return 0; - if (queue < 0x20 || queue >= 0x58) { + if (queue < 0x20 || queue >= AgcDriver::DriverDetail::SubmissionQueueEnd) { throw std::runtime_error(std::string(__func__) + ": unsupported compute queue"); } return AgcDriver::SubmitMany(acbs, sizes_in_dwords, count, queue); diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp index 7b326f7f9d..b5e58e6871 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include <cerrno> #include <cstdint> @@ -15,7 +16,7 @@ extern "C" { int APS5_VABI sceAgcDriverSubmitCommandBuffer(void* queue_context, const Packet* packet) { if (!queue_context || !packet) return EINVAL; const auto queue = *reinterpret_cast<const std::uint32_t*>(static_cast<const char*>(queue_context) + 4); - if (queue != 0 && (queue < 0x20 || queue >= 0x58)) { + if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::SubmissionQueueEnd)) { throw std::runtime_error(std::string(__func__) + ": unsupported queue"); } AgcDriver::Submit(packet, queue); @@ -26,7 +27,7 @@ int APS5_VABI sceAgcDriverSubmitMultiCommandBuffers(void* queue_context, uint32_ if (count == 0) return 0; if (!queue_context || !command_buffers || !sizes_in_dwords) return EINVAL; const auto queue = *reinterpret_cast<const std::uint32_t*>(static_cast<const char*>(queue_context) + 4); - if (queue != 0 && (queue < 0x20 || queue >= 0x58)) { + if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::SubmissionQueueEnd)) { throw std::runtime_error(std::string(__func__) + ": unsupported queue"); } return AgcDriver::SubmitMany(command_buffers, sizes_in_dwords, count, queue); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index e01a2fe1b3..cac90e5f01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -88,7 +88,9 @@ void testValidation() { expectFailure([] { sceAgcDriverAgrSubmitDcb(nullptr); }); expectFailure([] { sceAgcDriverSubmitAcb(0x20, nullptr); }); expectFailure([&] { sceAgcDriverSubmitAcb(0, &packet); }); - expectFailure([&] { sceAgcDriverSubmitAcb(0x58, &packet); }); + check(sceAgcDriverSubmitAcb(0x58, &packet) == 0, "queue 0x58 submit failed"); + AgcDriverWaitIdle_nid_postfix(); + expectFailure([&] { sceAgcDriverSubmitAcb(0x59, &packet); }); packet.dw_num = 2; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); packet.dw_num = 3; @@ -450,7 +452,7 @@ void testMultiAcbs() { std::array<std::uint32_t*, 1> addresses{write.data()}; std::array<std::uint32_t, 1> sizes{static_cast<std::uint32_t>(write.size())}; expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x1f, addresses.data(), sizes.data(), 1); }); - expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x58, addresses.data(), sizes.data(), 1); }); + expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x59, addresses.data(), sizes.data(), 1); }); expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0, addresses.data(), sizes.data(), 1); }); check(sceAgcDriverSubmitMultiAcbs(0, nullptr, nullptr, 0) == 0, "empty multi-ACB submit checked its queue"); expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, nullptr, sizes.data(), 1); }); @@ -459,7 +461,7 @@ void testMultiAcbs() { std::array<std::uint32_t, 1> nonZeroAcbSizes{4}; expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, nullAcbs.data(), nonZeroAcbSizes.data(), 1); }); check(value == 0, "a rejected multi-ACB submit ran a command buffer"); - for (std::uint32_t queue : {0x20u, 0x57u}) { + for (std::uint32_t queue : {0x20u, 0x57u, 0x58u}) { value = 0; done = 0; auto first = writeData(&value, 1); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b0dcdfef34..612ea3f7a3 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -10,6 +10,7 @@ - [sceAgcDriverSubmitCommandBuffer, sceAgcDriverSubmitMultiCommandBuffers](../../../core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp) (libSceAgcDriver) - `queue_context` is treated as a pointer to context whose queue identifier is stored 4 bytes in, matching KytyPS5 (`src/libs/agc.cpp` `AgcDriverSubmitCommandBuffer` / `get_driver_queue`). A null context returns `EINVAL` (22). Whether the console validates other context fields before queue dispatch is unverified - [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured. Four titles (PPSA15246 at 0x71dc9b, PPSA13356 at 0xd0c7b, PPSA21480 at 0xa920b, PPSA35169 at 0x6d08bb) disassembled for this entry call it once each from the same small wrapper, with queue 0x20 + 8 * the low byte of its argument | the next byte, and return the result unchanged; which values occur is not shown. +- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x58 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and 0x58; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured - [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. - [sceAgcDriverSetFlip, sceAgcDriverSetWorkloadsActive, sceAgcDriverSetWorkloadComplete, sceAgcDriverUserDataWritePacket, sceAgcDriverUserDataWritePushMarker, sceAgcDriverUserDataWritePopMarker, sceAgcDriverUserDataWriteSetMarker](../../../core/libs/prx/libSceAgcDriver/Export.cpp) (libSceAgcDriver) - declared without parameters; they throw through `NotImplemented_nid_no_patch`. The tree answers their packet sizes (`sceAgcDriverGetSetFlipPacketSizeInDwords`, `sceAgcDriverGetSetWorkloadsActivePacketSize`, `sceAgcDriverGetSetWorkloadCompletePacketSize`, `sceAgcDriverUserDataGetPacketSize` in `Submit/src/Dcb.cpp`), and libSceAgc writes its own flip, workload and marker packets through a `CommandBuffer*` (`libSceAgc/DcbState/src/Display.cpp`, `Workload.cpp`, `Marker.cpp`), but it has no driver-level prototype, so whether these calls take the same arguments and write the same words is not known From 683b3497dee095ad73f8e112b44b481255126c8c Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Fri, 23 Oct 2026 23:54:06 +0400 Subject: [PATCH 693/702] fix(agc): limit 0x58 acceptance to SubmitAcb Keep MultiAcbs and CommandBuffer on the unmeasured 0x57 exclusive end; split the debt entries and retain the title call-sites. --- .../prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp | 2 +- core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp | 2 +- core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp | 5 ++--- core/libs/prx/libSceAgcDriver/tests/Submit.cpp | 4 ++-- docs/dev/debt/libSceAgcDriver.md | 4 +++- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp index 606d6e0554..dcd319d36d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp @@ -176,7 +176,7 @@ std::string_view SubmissionCapture::KindName(SubmissionKind kind) { std::string_view SubmissionCapture::QueueType(std::uint32_t queue) { if (queue == 0) return "graphics"; if (queue >= 0x20 && queue < DriverDetail::SubmissionQueueEnd) return "compute"; - char what[64]; + char what[96]; std::snprintf(what, sizeof(what), "queue 0x%x is neither the graphics queue nor a compute queue", queue); throw std::runtime_error(std::string("submission capture: ") + what); } diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp index c6bda0c24a..9b4d131eb9 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp @@ -20,7 +20,7 @@ int APS5_VABI sceAgcDriverSubmitAcb(uint32_t queue, const Packet* packet) { int APS5_VABI sceAgcDriverSubmitMultiAcbs(uint32_t queue, uint32_t* const* acbs, const uint32_t* sizes_in_dwords, uint32_t count) { if (count == 0) return 0; - if (queue < 0x20 || queue >= AgcDriver::DriverDetail::SubmissionQueueEnd) { + if (queue < 0x20 || queue >= 0x58) { throw std::runtime_error(std::string(__func__) + ": unsupported compute queue"); } return AgcDriver::SubmitMany(acbs, sizes_in_dwords, count, queue); diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp index b5e58e6871..7b326f7f9d 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp @@ -1,7 +1,6 @@ #include "prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" -#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include <cerrno> #include <cstdint> @@ -16,7 +15,7 @@ extern "C" { int APS5_VABI sceAgcDriverSubmitCommandBuffer(void* queue_context, const Packet* packet) { if (!queue_context || !packet) return EINVAL; const auto queue = *reinterpret_cast<const std::uint32_t*>(static_cast<const char*>(queue_context) + 4); - if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::SubmissionQueueEnd)) { + if (queue != 0 && (queue < 0x20 || queue >= 0x58)) { throw std::runtime_error(std::string(__func__) + ": unsupported queue"); } AgcDriver::Submit(packet, queue); @@ -27,7 +26,7 @@ int APS5_VABI sceAgcDriverSubmitMultiCommandBuffers(void* queue_context, uint32_ if (count == 0) return 0; if (!queue_context || !command_buffers || !sizes_in_dwords) return EINVAL; const auto queue = *reinterpret_cast<const std::uint32_t*>(static_cast<const char*>(queue_context) + 4); - if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::SubmissionQueueEnd)) { + if (queue != 0 && (queue < 0x20 || queue >= 0x58)) { throw std::runtime_error(std::string(__func__) + ": unsupported queue"); } return AgcDriver::SubmitMany(command_buffers, sizes_in_dwords, count, queue); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index cac90e5f01..d40bd5ed6c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -452,7 +452,7 @@ void testMultiAcbs() { std::array<std::uint32_t*, 1> addresses{write.data()}; std::array<std::uint32_t, 1> sizes{static_cast<std::uint32_t>(write.size())}; expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x1f, addresses.data(), sizes.data(), 1); }); - expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x59, addresses.data(), sizes.data(), 1); }); + expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x58, addresses.data(), sizes.data(), 1); }); expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0, addresses.data(), sizes.data(), 1); }); check(sceAgcDriverSubmitMultiAcbs(0, nullptr, nullptr, 0) == 0, "empty multi-ACB submit checked its queue"); expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, nullptr, sizes.data(), 1); }); @@ -461,7 +461,7 @@ void testMultiAcbs() { std::array<std::uint32_t, 1> nonZeroAcbSizes{4}; expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, nullAcbs.data(), nonZeroAcbSizes.data(), 1); }); check(value == 0, "a rejected multi-ACB submit ran a command buffer"); - for (std::uint32_t queue : {0x20u, 0x57u, 0x58u}) { + for (std::uint32_t queue : {0x20u, 0x57u}) { value = 0; done = 0; auto first = writeData(&value, 1); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 612ea3f7a3..7d413e0960 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -7,10 +7,12 @@ ### Unknown function info -- [sceAgcDriverSubmitCommandBuffer, sceAgcDriverSubmitMultiCommandBuffers](../../../core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp) (libSceAgcDriver) - `queue_context` is treated as a pointer to context whose queue identifier is stored 4 bytes in, matching KytyPS5 (`src/libs/agc.cpp` `AgcDriverSubmitCommandBuffer` / `get_driver_queue`). A null context returns `EINVAL` (22). Whether the console validates other context fields before queue dispatch is unverified +- [sceAgcDriverSubmitCommandBuffer, sceAgcDriverSubmitMultiCommandBuffers](../../../core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp) (libSceAgcDriver) - `queue_context` is treated as a pointer to context whose queue identifier is stored 4 bytes in, matching KytyPS5 (`src/libs/agc.cpp` `AgcDriverSubmitCommandBuffer` / `get_driver_queue`). A null context returns `EINVAL` (22). Whether the console validates other context fields before queue dispatch is unverified. Compute queues are still accepted only for 0x20 to 0x57; queue 0x58 and above throw. Console acceptance of 0x58 on these entrypoints was not measured - [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured. Four titles (PPSA15246 at 0x71dc9b, PPSA13356 at 0xd0c7b, PPSA21480 at 0xa920b, PPSA35169 at 0x6d08bb) disassembled for this entry call it once each from the same small wrapper, with queue 0x20 + 8 * the low byte of its argument | the next byte, and return the result unchanged; which values occur is not shown. - [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x58 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and 0x58; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured +- [sceAgcDriverSubmitAcb](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accepts compute queues 0x20 to 0x58 and throws for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) it returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and 0x58; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 were not measured. Four titles (PPSA15246 at 0x71dc9b, PPSA13356 at 0xd0c7b, PPSA21480 at 0xa920b, PPSA35169 at 0x6d08bb) disassembled for this entry call it once each from the same small wrapper, with queue 0x20 + 8 * the low byte of its argument | the next byte, and return the result unchanged; which values occur is not shown. Closes when: the console's highest accepted compute queue is measured +- [sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) still accepts only compute queues 0x20 to 0x57; queue 0x58 and above throw. Console acceptance of 0x58 on this entrypoint was not measured - [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. - [sceAgcDriverSetFlip, sceAgcDriverSetWorkloadsActive, sceAgcDriverSetWorkloadComplete, sceAgcDriverUserDataWritePacket, sceAgcDriverUserDataWritePushMarker, sceAgcDriverUserDataWritePopMarker, sceAgcDriverUserDataWriteSetMarker](../../../core/libs/prx/libSceAgcDriver/Export.cpp) (libSceAgcDriver) - declared without parameters; they throw through `NotImplemented_nid_no_patch`. The tree answers their packet sizes (`sceAgcDriverGetSetFlipPacketSizeInDwords`, `sceAgcDriverGetSetWorkloadsActivePacketSize`, `sceAgcDriverGetSetWorkloadCompletePacketSize`, `sceAgcDriverUserDataGetPacketSize` in `Submit/src/Dcb.cpp`), and libSceAgc writes its own flip, workload and marker packets through a `CommandBuffer*` (`libSceAgc/DcbState/src/Display.cpp`, `Workload.cpp`, `Marker.cpp`), but it has no driver-level prototype, so whether these calls take the same arguments and write the same words is not known From 5d45d2cab7dcae3b0bc2315479e2eb027c7728c2 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Wed, 28 Oct 2026 02:56:56 +0400 Subject: [PATCH 694/702] fix(agc): name unmeasured queue end and drop debt duplicates --- .../Execution/include/Driver/Queues/SubmissionRing.hpp | 1 + core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp | 2 +- core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp | 5 +++-- docs/dev/debt/libSceAgcDriver.md | 2 -- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp index 5d19421c5a..52776f22e6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp @@ -10,6 +10,7 @@ namespace AgcDriver::DriverDetail { inline constexpr std::uint32_t SubmissionQueueEnd = 0x59; +inline constexpr std::uint32_t UnmeasuredSubmissionQueueEnd = 0x58; std::size_t SubmissionRingCapacity(); diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp index 9b4d131eb9..628aef3851 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp @@ -20,7 +20,7 @@ int APS5_VABI sceAgcDriverSubmitAcb(uint32_t queue, const Packet* packet) { int APS5_VABI sceAgcDriverSubmitMultiAcbs(uint32_t queue, uint32_t* const* acbs, const uint32_t* sizes_in_dwords, uint32_t count) { if (count == 0) return 0; - if (queue < 0x20 || queue >= 0x58) { + if (queue < 0x20 || queue >= AgcDriver::DriverDetail::UnmeasuredSubmissionQueueEnd) { throw std::runtime_error(std::string(__func__) + ": unsupported compute queue"); } return AgcDriver::SubmitMany(acbs, sizes_in_dwords, count, queue); diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp index 7b326f7f9d..a2c4534fdc 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include <cerrno> #include <cstdint> @@ -15,7 +16,7 @@ extern "C" { int APS5_VABI sceAgcDriverSubmitCommandBuffer(void* queue_context, const Packet* packet) { if (!queue_context || !packet) return EINVAL; const auto queue = *reinterpret_cast<const std::uint32_t*>(static_cast<const char*>(queue_context) + 4); - if (queue != 0 && (queue < 0x20 || queue >= 0x58)) { + if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::UnmeasuredSubmissionQueueEnd)) { throw std::runtime_error(std::string(__func__) + ": unsupported queue"); } AgcDriver::Submit(packet, queue); @@ -26,7 +27,7 @@ int APS5_VABI sceAgcDriverSubmitMultiCommandBuffers(void* queue_context, uint32_ if (count == 0) return 0; if (!queue_context || !command_buffers || !sizes_in_dwords) return EINVAL; const auto queue = *reinterpret_cast<const std::uint32_t*>(static_cast<const char*>(queue_context) + 4); - if (queue != 0 && (queue < 0x20 || queue >= 0x58)) { + if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::UnmeasuredSubmissionQueueEnd)) { throw std::runtime_error(std::string(__func__) + ": unsupported queue"); } return AgcDriver::SubmitMany(command_buffers, sizes_in_dwords, count, queue); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7d413e0960..35a39da6e0 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -9,8 +9,6 @@ - [sceAgcDriverSubmitCommandBuffer, sceAgcDriverSubmitMultiCommandBuffers](../../../core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp) (libSceAgcDriver) - `queue_context` is treated as a pointer to context whose queue identifier is stored 4 bytes in, matching KytyPS5 (`src/libs/agc.cpp` `AgcDriverSubmitCommandBuffer` / `get_driver_queue`). A null context returns `EINVAL` (22). Whether the console validates other context fields before queue dispatch is unverified. Compute queues are still accepted only for 0x20 to 0x57; queue 0x58 and above throw. Console acceptance of 0x58 on these entrypoints was not measured -- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured. Four titles (PPSA15246 at 0x71dc9b, PPSA13356 at 0xd0c7b, PPSA21480 at 0xa920b, PPSA35169 at 0x6d08bb) disassembled for this entry call it once each from the same small wrapper, with queue 0x20 + 8 * the low byte of its argument | the next byte, and return the result unchanged; which values occur is not shown. -- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x58 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and 0x58; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured - [sceAgcDriverSubmitAcb](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accepts compute queues 0x20 to 0x58 and throws for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) it returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and 0x58; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 were not measured. Four titles (PPSA15246 at 0x71dc9b, PPSA13356 at 0xd0c7b, PPSA21480 at 0xa920b, PPSA35169 at 0x6d08bb) disassembled for this entry call it once each from the same small wrapper, with queue 0x20 + 8 * the low byte of its argument | the next byte, and return the result unchanged; which values occur is not shown. Closes when: the console's highest accepted compute queue is measured - [sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) still accepts only compute queues 0x20 to 0x57; queue 0x58 and above throw. Console acceptance of 0x58 on this entrypoint was not measured From 4a7c7490479e4fecbddce5a6e7fe18fdaf957ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= <adriadam10@gmail.com> Date: Wed, 28 Oct 2026 08:59:32 +0100 Subject: [PATCH 695/702] fix(agc): leave the capture error buffer width to #7555 --- .../prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp index dcd319d36d..606d6e0554 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp @@ -176,7 +176,7 @@ std::string_view SubmissionCapture::KindName(SubmissionKind kind) { std::string_view SubmissionCapture::QueueType(std::uint32_t queue) { if (queue == 0) return "graphics"; if (queue >= 0x20 && queue < DriverDetail::SubmissionQueueEnd) return "compute"; - char what[96]; + char what[64]; std::snprintf(what, sizeof(what), "queue 0x%x is neither the graphics queue nor a compute queue", queue); throw std::runtime_error(std::string("submission capture: ") + what); } From dfc5c7c6c7f8c4cf5c6c23f0c7a0b607f191088c Mon Sep 17 00:00:00 2001 From: edubox <edubox@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:37:07 +0100 Subject: [PATCH 696/702] fix(ngs2): reset the custom filter selected by the parameter index --- .../libs/prx/libSceNgs2.native/src/Custom.cpp | 3 + .../libSceNgs2.native/tests/Ngs2Custom.cpp | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 2fb63ebcc8..b272d7da30 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -263,6 +263,9 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { filter.channel_mask = custom.channel_mask; if (custom.type == 0) { filter = {}; + filter.index = index; + filter.location = 1; + filter.type = 0; } else if (custom.type == 1) { filter.frequency = custom.param.fcq.frequency; filter.q = custom.param.fcq.q; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 4e8f3999b7..0741982ce4 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -257,6 +257,60 @@ static void TestRackOptions() { Require(calls.cleanupVoices == (std::vector<std::uint32_t>{0, 1})); } +static void TestCustomFilterResetIndex() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + + Ngs2CustomSamplerRackOption option{}; + auto& common = option.custom_rack_option.rack_option; + common.size = sizeof(option); + common.max_grain_samples = 512; + common.max_voices = 1; + common.max_matrices = 1; + common.max_ports = 1; + option.custom_rack_option.num_buffers = 1; + option.custom_rack_option.num_modules = 2; + for (auto& module : option.custom_rack_option.module) { + module.module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; + } + option.max_channel_works = 1; + + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const std::vector<std::int16_t> pcm(Grain * 4, 16384); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 1, 0, static_cast<std::uint32_t>(pcm.size()), 0, 0}; + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + + Ngs2CustomVoiceFilterParam mute{}; + mute.type = 1; + mute.param.fcq.frequency = 1000.0f; + mute.param.fcq.q = 0.70710678f; + mute.param.fcq.level = 0.0f; + Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER | 0, mute); + std::vector<float> out(Grain, -1.0f); + const Ngs2RenderBufferInfo renderInfo{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); + for (const float sample : out) Require(sample == 0.0f); + + Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER | 1, Ngs2CustomVoiceFilterParam{}); + out.assign(Grain, -1.0f); + Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); + for (const float sample : out) Require(sample == 0.0f); + + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestCustomSampler() { calls = {}; const auto system = CreateSystem(); @@ -901,6 +955,7 @@ int main() { TestUserFxChain(); TestRackOptions(); TestCustomRackChecks(); + TestCustomFilterResetIndex(); TestCustomSampler(); TestCustomFilter(); for (const auto moduleIndex : {0u, 1u, 23u}) TestCustomLegacyUserFx(moduleIndex); From 2129d1f7b5d2d4814d768fb616547e5b9e94e600 Mon Sep 17 00:00:00 2001 From: frankea <frankea@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:59:44 -0400 Subject: [PATCH 697/702] fix(libSceNet): print the interface and socket tables from sceNetShowIfconfig and sceNetShowNetstat Since #9137 both throw after sceNetInit. A PS5 on firmware 13.40 returns 0 from each and prints plain-text tables to stdout. They now print the console's layout with what the emulator knows: ifconfig an eth0 block from the host interface sceNetCtlGetInfo reports (the same MAC, address, netmask and MTU), the console's lo0 block and the DNS servers; netstat the header and one entry per socket and epoll of the title, with the self and non-blocking flags, addresses, buffer sizes and the names given to sceNetSocket and sceNetEpollCreate, which are now kept. The debt entry lists what the console prints that is left out. guest_sce_net captures both outputs: the lo0 block and DNS section, a named unbound UDP socket, a non-blocking listening TCP socket bound to 127.0.0.1, an epoll, a 40-character name cut to 31, and the entries gone after close. --- core/libs/prx/libSceNet/Export.cpp | 107 +++++++++++++++++++++++--- core/libs/prx/libSceNetCtl/Export.cpp | 9 +++ core/libs/tests/GuestSceNet.cpp | 57 +++++++++++--- docs/dev/debt/libSceNet.md | 2 +- 4 files changed, 153 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 9f8a9f868a..29c7a2d2dd 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -289,6 +289,7 @@ struct Sock { int rcv_buffer = 0; int onesbcast = 0; int keepalive = 0; + std::string name; }; #ifdef _WIN32 @@ -345,6 +346,7 @@ HostMutex g_mutex; HostCondition g_cv; std::map<int, Sock> g_socks; std::set<int> g_epolls; +std::map<int, std::string> g_epoll_names; using EpollRegistrations = std::map<int, std::shared_ptr<const NetEpollEvent>>; std::map<int, EpollRegistrations> g_epoll_socks; std::set<int> g_epoll_aborted; @@ -373,6 +375,7 @@ int* errno_slot() { } extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity); +extern "C" bool NetCtlHostIpv4Address_nid_no_patch(std::uint32_t* address, std::uint32_t* netmask, std::uint32_t* mtu); std::mutex g_dnsLock; bool g_dnsOverridden = false; @@ -745,12 +748,6 @@ int* APS5_VABI sceNetErrnoLoc(void) { return errno_slot(); } -int APS5_VABI sceNetShowNetstat(void) { - std::lock_guard lock(g_mutex); - if (!g_net_inited) return fail(NET_ENOTINIT); - throw std::runtime_error(__func__); -} - int PollSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { std::vector<std::shared_ptr<NativeSocketHandle>> natives(static_cast<std::size_t>(count)); { @@ -916,7 +913,6 @@ int APS5_VABI sceNetPoolDestroy(int memid) { } int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) { - (void)name; if (family != NET_AF_INET && family != NET_AF_INET6) { return fail(NET_EAFNOSUPPORT); } @@ -963,6 +959,7 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) if (protocol == 0 && type == NET_SOCK_STREAM) s.protocol = IPPROTO_TCP; if (protocol == 0 && type == NET_SOCK_DGRAM) s.protocol = IPPROTO_UDP; s.native = std::move(handle); + if (name != nullptr) s.name = std::string(name).substr(0, 31); g_socks[fd] = s; return fd; } @@ -1927,10 +1924,70 @@ int APS5_VABI sceNetEpollCreate(const char* name, int flags) { std::lock_guard lk(g_mutex); const int id = g_next_epoll++; g_epolls.insert(id); + g_epoll_names[id] = name != nullptr ? std::string(name).substr(0, 31) : std::string(); g_epoll_socks.emplace(id, EpollRegistrations{}); return id; } +static std::string endpoint_text(const sockaddr_storage& address) { + char text[INET6_ADDRSTRLEN] = {}; + std::uint16_t port = 0; + std::string host; + if (address.ss_family == AF_INET6) { + const auto& v6 = reinterpret_cast<const sockaddr_in6&>(address); + static constexpr in6_addr any{}; + host = std::memcmp(&v6.sin6_addr, &any, sizeof(any)) == 0 || inet_ntop(AF_INET6, &v6.sin6_addr, text, sizeof(text)) == nullptr ? "[*]" : std::string("[") + text + "]"; + port = ntohs(v6.sin6_port); + } else { + const auto& v4 = reinterpret_cast<const sockaddr_in&>(address); + host = address.ss_family != AF_INET || v4.sin_addr.s_addr == 0 || inet_ntop(AF_INET, &v4.sin_addr, text, sizeof(text)) == nullptr ? "*" : std::string(text); + port = address.ss_family == AF_INET ? ntohs(v4.sin_port) : 0; + } + return host + ":" + (port == 0 ? std::string("*") : std::to_string(port)); +} + +int APS5_VABI sceNetShowNetstat(void) { + std::vector<std::pair<int, Sock>> sockets; + std::vector<std::pair<int, std::string>> epolls; + { + std::lock_guard lock(g_mutex); + if (!g_net_inited) return fail(NET_ENOTINIT); + sockets.assign(g_socks.begin(), g_socks.end()); + epolls.assign(g_epoll_names.begin(), g_epoll_names.end()); + } + const int pid = getpid_nid_postfix(); + std::printf("--- ID, type, [self/R/S/RE/SE//NB], ... ---\n"); + for (const int type : {NET_SOCK_DGRAM, NET_SOCK_STREAM, NET_SOCK_RAW}) { + for (auto it = sockets.rbegin(); it != sockets.rend(); ++it) { + const auto& [id, socket] = *it; + if (socket.type != type) continue; + sockaddr_storage local{}; + sockaddr_storage remote{}; + NativeLength length = sizeof(local); + if (::getsockname(socket.native->value, reinterpret_cast<sockaddr*>(&local), &length) != 0) local = {}; + length = sizeof(remote); + const bool connected = ::getpeername(socket.native->value, reinterpret_cast<sockaddr*>(&remote), &length) == 0; + if (!connected) remote = {}; + local.ss_family = remote.ss_family = socket.family == NET_AF_INET6 ? AF_INET6 : AF_INET; + std::int32_t send = 0; + std::int32_t receive = 0; + if (sock_buffer_size(socket, true, send) != 0) send = 0; + if (sock_buffer_size(socket, false, receive) != 0) receive = 0; + const char* kind = type == NET_SOCK_STREAM ? "tcp" : type == NET_SOCK_DGRAM ? "udp" : "raw"; + const char* state = type != NET_SOCK_STREAM ? "OPENED" : socket.listening ? "LISTEN" : connected ? "ESTABLISHED" : "CLOSED"; + std::printf("ID: %4d (%5s, 0) *....-%c %s pid=0x%08x %s\n", id, kind, socket.native->nonblock ? '*' : '.', state, static_cast<unsigned>(pid), socket.name.c_str()); + std::printf(" Local : %s%28s\n", endpoint_text(local).c_str(), ""); + std::printf(" Remote: %s%28s\n", endpoint_text(remote).c_str(), ""); + std::printf(" Send-Q: 0/%d TX: 0bps/0bps, dropped 0 (0) \n", send); + std::printf(" Recv-Q: 0/%d RX: 0bps/0bps, dropped 0\n", receive); + } + } + for (auto it = epolls.rbegin(); it != epolls.rend(); ++it) + std::printf("ID: %4d (epoll) *....-. OPENED pid=0x%08x %s\n", it->first, static_cast<unsigned>(pid), it->second.c_str()); + std::fflush(stdout); + return 0; +} + int APS5_VABI sceNetShowNetstatWithMemory(int memid) { struct SocketStatus { int id; int family; int type; bool nonblock; bool bound; bool listening; }; std::vector<SocketStatus> sockets; @@ -1968,6 +2025,7 @@ int APS5_VABI sceNetEpollDestroy(int eid) { if (g_epolls.erase(eid) == 0) { return fail(NET_EBADF); } + g_epoll_names.erase(eid); g_epoll_socks.erase(eid); g_epoll_aborted.erase(eid); g_cv.NotifyAll(); @@ -2435,9 +2493,38 @@ int APS5_VABI sceNetResolverAbort(int rid, int flags) { } int APS5_VABI sceNetShowIfconfig() { - std::lock_guard lock(g_mutex); - if (!g_net_inited) return fail(NET_ENOTINIT); - throw std::runtime_error(__func__); + { + std::lock_guard lock(g_mutex); + if (!g_net_inited) return fail(NET_ENOTINIT); + } + const auto dotted = [](std::uint32_t address) { + char text[INET_ADDRSTRLEN] = {}; + in_addr value{}; + value.s_addr = address; + return std::string(inet_ntop(AF_INET, &value, text, sizeof(text)) != nullptr ? text : "0.0.0.0"); + }; + std::uint32_t address = 0; + std::uint32_t netmask = 0; + std::uint32_t mtu = 0; + if (NetCtlHostIpv4Address_nid_no_patch(&address, &netmask, &mtu)) { + NetEtherAddr mac{}; + sceNetGetMacAddress(&mac, 0); + std::printf("eth0 (index=1)\n"); + std::printf(" Type:Ethernet HWaddr:%02x:%02x:%02x:%02x:%02x:%02x\n", mac.data[0], mac.data[1], mac.data[2], mac.data[3], mac.data[4], mac.data[5]); + std::printf(" inet:%s/%s Bcast:%s\n", dotted(address).c_str(), dotted(netmask).c_str(), dotted(address | ~netmask).c_str()); + std::printf(" UP BROADCAST MULTICAST INTERNET CONNECTED MTU:%u(max:%u) \n", mtu, mtu); + } + std::printf("lo0 (index=2)\n"); + std::printf(" inet:127.0.0.1/255.0.0.0\n"); + std::printf(" inet6:::1/128\n"); + std::printf(" UP MULTICAST CONNECTED MTU:16384(max:16384) \n"); + std::array<std::uint32_t, 2> servers{}; + NetCtlHostDnsServers_nid_no_patch(servers.data(), servers.size()); + std::printf("dns addresses\n"); + std::printf(" %s, %s\n", dotted(servers[0]).c_str(), dotted(servers[1]).c_str()); + std::printf(" ::, ::\n"); + std::fflush(stdout); + return 0; } extern const std::uint8_t in6addr_any_nid_postfix[16] = {}; diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index c3cb3b3a08..43277e29bf 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -822,6 +822,15 @@ static NetCtlNatInfo DiscoverNatInfo(in_addr localAddress) { return result; } +extern "C" bool NetCtlHostIpv4Address_nid_no_patch(std::uint32_t* address, std::uint32_t* netmask, std::uint32_t* mtu) { + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return false; + *address = host.address.s_addr; + *netmask = host.netmask.s_addr; + *mtu = host.mtu; + return true; +} + static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { std::memcpy(info->opaque, &value, sizeof(value)); } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 89578a68c7..963c1eb14a 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -176,7 +176,8 @@ static void CheckStreamSender(int family) { for (const int descriptor : {client, accepted, listener}) Require(sceNetSocketClose(descriptor) == 0); } -static std::string CaptureNetstat(int pool) { +template <typename Diagnostic> +static std::string CaptureDiagnostic(Diagnostic diagnostic) { auto* file = std::tmpfile(); Require(file != nullptr); Require(std::fflush(stdout) == 0); @@ -189,7 +190,7 @@ static std::string CaptureNetstat(int pool) { const int saved = dup(output); Require(saved >= 0 && dup2(fileno(file), output) == output); #endif - const int result = sceNetShowNetstatWithMemory(pool); + const int result = diagnostic(); Require(std::fflush(stdout) == 0); #ifdef _WIN32 Require(_dup2(saved, output) == 0 && _close(saved) == 0); @@ -204,16 +205,50 @@ static std::string CaptureNetstat(int pool) { return text; } +static std::string CaptureNetstat(int pool) { + return CaptureDiagnostic([pool] { return sceNetShowNetstatWithMemory(pool); }); +} + +static std::string NetstatEntry(int id, const char* kind, char nonblocking, const char* state, const std::string& name) { + std::array<char, 160> line{}; + std::snprintf(line.data(), line.size(), "ID: %4d (%5s, 0) *....-%c %s pid=0x%08x ", id, kind, nonblocking, state, static_cast<unsigned>(getpid_nid_postfix())); + return line.data() + name + "\n"; +} + +static void CheckDiagnostics() { + const auto ifconfig = CaptureDiagnostic(sceNetShowIfconfig); + Require(ifconfig.find("lo0 (index=2)\n inet:127.0.0.1/255.0.0.0\n inet6:::1/128\n UP MULTICAST CONNECTED MTU:16384(max:16384) \n") != std::string::npos); + Require(ifconfig.find("\ndns addresses\n ") != std::string::npos && ifconfig.ends_with("\n ::, ::\n")); + Require(ifconfig.starts_with("lo0 (index=2)\n") || ifconfig.starts_with("eth0 (index=1)\n Type:Ethernet HWaddr:02:50:53:35:00:01\n inet:")); + const std::string padding(28, ' '); + const std::string longName(40, 'x'); + const int udp = sceNetSocket(longName.c_str(), 2, 2, 0); + const int tcp = sceNetSocket("netstat-tcp", 2, 1, 0); + const int epoll = sceNetEpollCreate("netstat-epoll", 0); + Require(udp >= 0 && tcp >= 0 && epoll >= 0); + const int nonblocking = 1; + Require(sceNetSetsockopt(tcp, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + std::array<std::uint8_t, 16> address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(tcp, address.data(), address.size()) == 0 && sceNetListen(tcp, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(tcp, address.data(), &size) == 0); + const auto port = std::to_string((address[2] << 8) | address[3]); + const auto netstat = CaptureDiagnostic(sceNetShowNetstat); + Require(netstat.starts_with("--- ID, type, [self/R/S/RE/SE//NB], ... ---\n")); + const auto udpAt = netstat.find(NetstatEntry(udp, "udp", '.', "OPENED", longName.substr(0, 31)) + " Local : *:*" + padding + "\n Remote: *:*" + padding + "\n Send-Q: 0/"); + const auto tcpAt = netstat.find(NetstatEntry(tcp, "tcp", '*', "LISTEN", "netstat-tcp") + " Local : 127.0.0.1:" + port + padding + "\n Remote: *:*" + padding + "\n Send-Q: 0/"); + std::array<char, 96> epollLine{}; + std::snprintf(epollLine.data(), epollLine.size(), "ID: %4d (epoll) *....-. OPENED pid=0x%08x netstat-epoll\n", epoll, static_cast<unsigned>(getpid_nid_postfix())); + const auto epollAt = netstat.find(epollLine.data()); + Require(udpAt != std::string::npos && tcpAt != std::string::npos && epollAt != std::string::npos && udpAt < tcpAt && tcpAt < epollAt); + Require(netstat.find(longName.substr(0, 32)) == std::string::npos); + Require(sceNetSocketClose(udp) == 0 && sceNetSocketClose(tcp) == 0 && sceNetEpollDestroy(epoll) == 0); + const auto closed = CaptureDiagnostic(sceNetShowNetstat); + Require(closed.find("netstat-tcp") == std::string::npos && closed.find("netstat-epoll") == std::string::npos && closed.find(longName.substr(0, 31)) == std::string::npos); +} + static void CheckNetstat() { - for (const auto diagnostic : {sceNetShowNetstat, sceNetShowIfconfig}) { - bool unsupported = false; - try { - diagnostic(); - } catch (const std::runtime_error&) { - unsupported = true; - } - Require(unsupported); - } + CheckDiagnostics(); Require(sceNetShowPolicy() == 0); Require(sceNetShowNetstatEx(0) == 0); Require(sceNetShowRoute() == 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 1b91a2824b..f016f29d10 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -3,7 +3,7 @@ ### Silent stubs - [sceNetShowRouteWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - validates memory pool ID and returns success without printing anything: the `(poolId)` signature and pool validation follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp) and `sceNetShowNetstatWithMemory`. What the console prints and where is unverified -- [sceNetShowIfconfig, sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) return the native not-initialized error and set errno before initialization. Initialized calls throw because native diagnostic collection and formatting are not implemented. +- [sceNetShowIfconfig, sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) print the console's layout (a PS5 on firmware 13.40, measured 2026-10-19) with what the emulator knows. Ifconfig prints `eth0` from the host interface `sceNetCtlGetInfo` reports, without its IPv6 address, `UP6`, link status, traffic counters or times, then `lo0` as the console does and the two IPv4 DNS servers, with `::, ::` for IPv6. It leaves out the wireless interfaces and the pool and statistics sections. Netstat lists only the title's sockets and epolls; the console lists every process's. The self and non-blocking flags, addresses, buffer sizes and names are filled; the R, S, RE and SE flags, the field after the type, queue lengths, rates and drops print as for an idle socket, TCP states are only `LISTEN`, `ESTABLISHED` and `CLOSED`, and the self flag on epolls is inferred. Before initialization both return the not-initialized error. - [sceNetShowPolicy, sceNetShowNetstatEx, sceNetShowRoute, sceNetShowRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - return success without printing anything: the `()` signatures and the success return follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net.cpp), which stubs each the same way. PPSA01530 fails to load without the `sceNetShowIfconfig` export (#3177); PPSA11386 imports `sceNetShowNetstat` but does not call it during start-up. What the console prints and where is unverified ### Unknown function info From f0cf07265c77c619c381328a0a76303ce1dc6abb Mon Sep 17 00:00:00 2001 From: dantdmdarealguy <vaibhav.vaidish.898@gmail.com> Date: Wed, 28 Oct 2026 03:20:36 -0500 Subject: [PATCH 698/702] docs(build): list ANYPS5_REQUIRE_CPU_LEAD_TEST and ANYPS5_AGC_EXECUTION_TESTS The CMake flag table left out the ANYPS5_REQUIRE_CPU_LEAD_TEST option and the ANYPS5_AGC_EXECUTION_TESTS cache list that the Windows, MSYS2 and sanitizer workflows set. Add a row for each and drop the stray comma after "the reason" in the sanitizer workflow paragraph. --- docs/dev/BUILD.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 1154ea812c..5d94c7cf19 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -121,10 +121,11 @@ Project switches accept `ON` or `OFF`: | `-DAPS5_ENABLE_TRACY=ON` | `OFF` | Build the [Tracy](#tracy) profiler client into the agc driver. | | `-DAPS5_AGC_CREATE_LOG=OFF` | `ON` | Disable successful `sceAgcCreateShader` logging. | | `-DAGC_BUILD_VISUAL_TEST=ON` | `OFF` | Build the standalone AGC SPIR-V visual test. | +| `-DANYPS5_REQUIRE_CPU_LEAD_TEST=ON` | `OFF` | With `BUILD_TESTING=ON`, also register `agc_driver_sparse_dma_buf_cpu_lead`, which runs `agc_driver_sparse_dma_buf_import_tests --require-cpu-lead` and fails instead of skipping when the nonzero-lead dma-buf CPU publish is not exercised, such as on Windows or without an accessible `/dev/udmabuf`. | GCC and Clang compile libc with `-ffp-contract=off` whatever the target CPU. Toolchains that default to x86-64-v3, such as GCC 15 on Ubuntu 26.04, would otherwise fuse `a * b + c` into FMA instructions, which changes the last bit of math results that must match the console. -The [Sanitizers workflow](../../.github/workflows/sanitizers.yml) builds the full runtime on Ubuntu with `-DANYPS5_ENABLE_ASAN=ON` and UBSan (`-fsanitize=undefined -fno-sanitize=vptr`) nightly at 03:41 UTC, runs every test and can be started manually with `workflow_dispatch`. It builds only the `F16ToF32Unpack`, `F64FmaSubnormals` and `RegistrationPrepareShutdown` AGC execution tests, like the Windows MSYS2 workflow. A test that fails because of the sanitizers themselves, or because of a bug whose fix is still open, is listed with the reason in [sanitizer-known-failures.txt](../../.github/sanitizer-known-failures.txt), one test per line with its sanitizers and the reason,. The job summary, written by `tools/sanitizer_report.py`, separates unexpected failures from known ones and names listed tests that passed or did not run, so their entries can be removed. To repeat a run locally, set `ASAN_OPTIONS=detect_leaks=0` and `UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1`, so that undefined behavior fails the test. +The [Sanitizers workflow](../../.github/workflows/sanitizers.yml) builds the full runtime on Ubuntu with `-DANYPS5_ENABLE_ASAN=ON` and UBSan (`-fsanitize=undefined -fno-sanitize=vptr`) nightly at 03:41 UTC, runs every test and can be started manually with `workflow_dispatch`. It builds only the `F16ToF32Unpack`, `F64FmaSubnormals` and `RegistrationPrepareShutdown` AGC execution tests, like the Windows MSYS2 workflow. A test that fails because of the sanitizers themselves, or because of a bug whose fix is still open, is listed with the reason in [sanitizer-known-failures.txt](../../.github/sanitizer-known-failures.txt), one test per line with its sanitizers and the reason. The job summary, written by `tools/sanitizer_report.py`, separates unexpected failures from known ones and names listed tests that passed or did not run, so their entries can be removed. To repeat a run locally, set `ASAN_OPTIONS=detect_leaks=0` and `UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1`, so that undefined behavior fails the test. Build configuration parameters: @@ -136,6 +137,7 @@ Build configuration parameters: | `-DCMAKE_C_COMPILER_LAUNCHER=ccache` | Optional C compiler cache; requires `ccache`. | | `-DCMAKE_CXX_COMPILER_LAUNCHER=ccache` | Optional C++ compiler cache; requires `ccache`. | | `-DFFMPEG_PREBUILT_DIR=<path>` | Unpacked FFmpeg package for the target platform; empty by default. | +| `-DANYPS5_AGC_EXECUTION_TESTS=<names>` | Semicolon-separated file names, without `.cpp`, of the `core/libs/prx/libSceAgcDriver/tests/execution` tests to build. Every other execution test is built from a stub that prints `skipped, this build leaves the test out with ANYPS5_AGC_EXECUTION_TESTS` and exits 77. A name with no source there fails the configure. Empty by default, which builds them all. | SDL and FreeType settings forced by the root `CMakeLists.txt` cannot be overridden with `-D`. From f638d790661e828acaf67c091203472fa9b72a1d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:27:28 +0000 Subject: [PATCH 699/702] fix(tools): refuse a progress output set whose reports are the same file write_site writes progress.json, the two badges, progress.svg and progress.html one after another. When two of those names already refer to the same file (a hardlink, for example), the later write replaced the earlier report through the alias: progress.json ended up holding the HTML page. The existing reports are now compared with samefile before anything is written, and the run fails naming the two aliased files and leaving every report unchanged. --- tools/progress.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tools/progress.py b/tools/progress.py index f5b1dff590..b2acd14921 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -503,8 +503,22 @@ def report(base, head): return "\n".join(lines) +SITE_REPORTS = ("progress.json", "badge-libraries.svg", "badge-shaders.svg", "progress.svg", "progress.html") + + +def require_distinct_reports(output): + paths = [output / name for name in SITE_REPORTS] + for index, path in enumerate(paths): + if not path.exists(): + continue + for other in paths[index + 1:]: + if other.exists() and path.samefile(other): + raise ValueError(f"{path.name} and {other.name} in {output} are the same file") + + def write_site(output, libraries, shaders): output.mkdir(parents=True, exist_ok=True) + require_distinct_reports(output) (output / "progress.json").write_text(json.dumps({"libraries": libraries, "shaders": shaders}, indent=2), encoding="utf-8") (output / "badge-libraries.svg").write_text(badge("libraries*", libraries), encoding="utf-8") (output / "badge-shaders.svg").write_text(badge("shaders", shaders), encoding="utf-8") @@ -592,6 +606,8 @@ def main(argv=None): if args.output: try: write_site(args.output, libraries, shaders) + except ValueError as error: + parser.error(str(error)) except OSError as error: parser.error(f"{args.output}: {error.strerror or error}") if args.json: From 80769379600be119784ba001b4b83e76a69eac3d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:27:42 +0000 Subject: [PATCH 700/702] test(tools): generate the progress site over two hardlinked reports progress.json and progress.html are the same file and the other reports exist; the command has to exit with 2 naming the two files, with no traceback, and every existing report keeps its bytes. --- tools/tests/test_progress.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index ab01001a31..96bcc273c6 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -677,6 +677,31 @@ class ProgressTests(unittest.TestCase): self.assertIn(str(blocked), stderr.getvalue()) self.assertNotIn("Traceback", stderr.getvalue()) + def test_aliased_reports_are_refused_and_left_unchanged(self): + libraries, shaders = self.library_snapshot() + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "site" + output.mkdir() + first, second = output / "progress.json", output / "progress.html" + first.write_text("json report", encoding="utf-8") + try: + second.hardlink_to(first) + except OSError: + self.skipTest("hard links are not available") + others = {name: output / name for name in ("badge-libraries.svg", "badge-shaders.svg", "progress.svg")} + for name, path in others.items(): + path.write_text(name, encoding="utf-8") + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + self.run_main([str(output)], libraries, shaders) + self.assertEqual(raised.exception.code, 2) + self.assertIn("progress.json and progress.html", stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + self.assertEqual(first.read_text(encoding="utf-8"), "json report") + self.assertEqual(second.read_text(encoding="utf-8"), "json report") + for name, path in others.items(): + self.assertEqual(path.read_text(encoding="utf-8"), name) + def test_root_must_be_a_source_tree(self): with tempfile.TemporaryDirectory() as directory: empty = Path(directory) / "empty tree" From b332780eecd13caeb2fc07766c05cf44470ba521 Mon Sep 17 00:00:00 2001 From: Dean <158058829+oneandonlydean@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:16:01 +1300 Subject: [PATCH 701/702] revert(shader): restore pixel helper lanes in guest lane masks (#11199) This reverts merge 7fc0ae6008. On NVIDIA, where helper lanes take part in ballots, clearing helper bits from every pixel ballot breaks derivatives along triangle edges (bright lines and specks on a cloud surface in PPSA21564). See the report on #11199. --- .../tests/execution/PixelHelperWaterfall.cpp | 201 ------------------ .../src/SpirvMemory/SpirvSubgroup.cpp | 7 - .../Translation/TranslationContext.hpp | 3 - .../src/ControlFlowInstructions.cpp | 6 +- .../Translation/src/InstructionTranslator.cpp | 6 - .../src/TranslationContext/ExecMask.cpp | 28 +-- .../src/TranslationContext/OperandAccess.cpp | 4 +- docs/dev/debt/shader.md | 2 +- 8 files changed, 11 insertions(+), 246 deletions(-) delete mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperWaterfall.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperWaterfall.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperWaterfall.cpp deleted file mode 100644 index 11d741c17a..0000000000 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelHelperWaterfall.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" -#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" -#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" -#include "Recompiler.hpp" -#include "VulkanTestDevice.hpp" -#include <algorithm> -#include <array> -#include <cstdio> -#include <iostream> -#include <map> -#include <string> -#include <vector> - -namespace { - -using AgcDriver::Graphics::Require; -using ShaderRecompiler::ShaderStage; - -constexpr std::uint32_t Sentinel = 0xdeadbeefu; -constexpr std::uint32_t Width = 192; -constexpr std::uint32_t Height = 128; -constexpr std::uint32_t Size = 16; -constexpr std::uint32_t Levels = 5; -constexpr std::uint32_t PassLimit = 80; -alignas(256) std::array<std::byte, Width * Height * 4> Pixels{}; -alignas(256) std::array<std::uint32_t, Width * Height * 2> Results{}; -alignas(256) std::array<std::uint8_t, 16384> Texels{}; - -alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{ - 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, -}; - -alignas(256) constexpr std::array<std::uint32_t, 42> PixelCode{ - 0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, 0x000000c0, 0xbec4047e, 0x7e120506, 0xbefe0a7e, - 0x101400ff, 0x3baaaaab, 0x101602ff, 0x3c000000, 0xf0800f08, 0x01060c0a, 0xbe8a047e, 0xbe8c0380, - 0xbe8d0380, 0xbe940480, 0xbe82140a, 0xd7600003, 0x00000506, 0x890d030d, 0xd4c20014, 0x00020c03, - 0x8a8a140a, 0x800c810c, 0xbf09ff0c, 0x00000050, 0xbf850002, 0xbf13800a, 0xbf85fff3, 0xbefe0444, - 0x8f0c900c, 0x880d0c0d, 0x7e100209, 0x7e12020d, 0xe0742000, 0x80010806, 0xbf8c3f70, 0xf800180f, - 0x0f0e0d0c, 0xbf810000, -}; - -std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { - const auto address = reinterpret_cast<std::uintptr_t>(data); - return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; -} - -std::array<std::uint32_t, 8> TextureDescriptor() { - const auto address = reinterpret_cast<std::uintptr_t>(Texels.data()); - return {static_cast<std::uint32_t>(address >> 8), static_cast<std::uint32_t>((address >> 40) & 0xffu) | (56u << 20) | (3u << 30), - ((Size - 1u) >> 2) | ((Size - 1u) << 14), 0xfacu | ((Levels - 1u) << 16) | (9u << 28), 0u, (Levels - 1u) << 4, 0u, 0u}; -} - -void FillTexture() { - const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor())); - Require(geometry.guestBytes <= Texels.size(), "helper waterfall texture does not fit"); - Texels.fill(0xffu); -} - -std::vector<std::array<float, 4>> Triangles() { - std::vector<std::array<float, 4>> vertices; - constexpr std::uint32_t columns = 6; - constexpr std::uint32_t rows = 4; - for (std::uint32_t cell = 0; cell < columns * rows; ++cell) { - const float width = 2.0f / columns; - const float height = 2.0f / rows; - const float x = -1.0f + width * static_cast<float>(cell % columns); - const float y = -1.0f + height * static_cast<float>(cell / columns); - const float skew = 0.07f * static_cast<float>(cell % 5u); - vertices.push_back({x + (0.05f + skew) * width, y + 0.05f * height, 0.5f, 1.0f}); - vertices.push_back({x + 0.93f * width, y + (0.11f + skew) * height, 0.5f, 1.0f}); - vertices.push_back({x + (0.37f - skew * 0.5f) * width, y + 0.91f * height, 0.5f, 1.0f}); - } - return vertices; -} - -void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) { - static const auto triangles = Triangles(); - Pixels.fill(std::byte{0}); - Results.fill(Sentinel); - - std::vector<std::uint32_t> vertexUserData(4, 0u); - const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast<std::uint32_t>(triangles.size())); - std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); - const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; - ShaderRecompiler::RecompileRequest vertex{ - {ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(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<std::uint32_t>(vertexResult.pushConstants.size()); - - 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); - std::vector<std::uint32_t> pixelUserData(36, 0u); - const auto results = BufferDescriptor(Results.data(), 8u, static_cast<std::uint32_t>(Results.size() / 2u)); - std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); - const auto texture = TextureDescriptor(); - std::copy(texture.begin(), texture.end(), pixelUserData.begin() + 24); - const std::array<std::uint32_t, 4> sampler{0u, 0xfffu << 12, 1u << 26, 0u}; - std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 32); - const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; - ShaderRecompiler::RecompileRequest fragment{ - {ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(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<AgcDriver::Graphics::CompiledShader, 2> 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<std::uintptr_t>(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<float>(Height), static_cast<float>(Width), -static_cast<float>(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<std::uint32_t>(triangles.size()), 0, 1, 0, false}; - device.Draw(state, draw, shaders); - device.WaitIdle(); -} - -void Check(std::uint32_t waveSize) { - const auto what = "pixel helper waterfall wave" + std::to_string(waveSize); - std::map<std::uint32_t, std::vector<std::uint32_t>> waves; - std::uint32_t partialQuads = 0; - for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { - if (Pixels[pixel * 4u + 3u] == std::byte{0}) { - Require(Results[pixel * 2u] == Sentinel, what + ": uncovered pixel " + std::to_string(pixel) + " stored a result"); - continue; - } - Require(Results[pixel * 2u] != Sentinel, what + ": covered pixel " + std::to_string(pixel) + " stored nothing"); - waves[Results[pixel * 2u]].push_back(pixel); - const auto quad = (pixel / Width & ~1u) * Width + (pixel % Width & ~1u); - if (quad == pixel) { - for (const auto offset : {1u, Width, Width + 1u}) { - if (Pixels[(quad + offset) * 4u + 3u] == std::byte{0}) { - ++partialQuads; - break; - } - } - } - } - Require(waves.size() > 4u && partialQuads > 16u, what + ": the triangles made only " + std::to_string(waves.size()) + " waves and " + std::to_string(partialQuads) + " partly covered quads"); - for (const auto& [first, live] : waves) { - std::uint32_t keys = 0; - for (const auto pixel : live) keys ^= pixel; - const auto passes = static_cast<std::uint32_t>(live.size()); - Require(passes < PassLimit, what + ": the wave whose first lane is pixel " + std::to_string(first) + " has more live lanes than the pass limit"); - for (const auto pixel : live) { - const auto stored = Results[pixel * 2u + 1u]; - Require((stored >> 16u) == passes, what + ": pixel " + std::to_string(pixel) + " of the wave whose first lane is pixel " + std::to_string(first) + " made " + std::to_string(stored >> 16u) + " passes over " + std::to_string(passes) + " live lanes"); - Require((stored & 0xffffu) == keys, what + ": pixel " + std::to_string(pixel) + " of the wave whose first lane is pixel " + std::to_string(first) + " read keys " + std::to_string(stored & 0xffffu) + ", expected " + std::to_string(keys)); - } - } -} - -} - -int main() { - try { - const auto device = OpenVulkanTestDevice(); - if (!device) return VulkanTestSkipped; - if (device->Target().subgroupSize < 32u) { - std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); - return VulkanTestSkipped; - } - FillTexture(); - for (const auto waveSize : {64u, 32u}) { - for (std::uint32_t pass = 0; pass < 4; ++pass) { - Draw(*device, waveSize, device->Target()); - Check(waveSize); - } - } - std::puts("pixel helper waterfall tests passed"); - return 0; - } catch (const std::exception& error) { - std::cerr << error.what() << '\n'; - return 1; - } -} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index 6348414594..4c0c787f07 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp @@ -100,13 +100,6 @@ namespace ShaderRecompiler state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), result, own, zero, zero, zero); return; } - if (state.program.Resources().stage == IrShaderStage::Pixel) { - if (const auto helperVariable = InputVariableForKind(state, StageInputKind::HelperInvocation); helperVariable != 0u) { - const auto helper = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeBool(state), helper, helperVariable); - predicate = Binary(state, spv::OpLogicalAnd, TypeBool(state), predicate, Unary(state, spv::OpLogicalNot, TypeBool(state), helper)); - } - } state.module.AddSubgroupBallot(TypeU32Vector(state, 4u), result, ConstantU32(state, spv::ScopeSubgroup), predicate); } diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 0d91ffc7e6..f51e2f016b 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -25,7 +25,6 @@ public: } void SetEntryRounding(const RoundingState& state) { setRounding(state); } void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; } - void SetWholeQuad(bool entered) { wholeQuad = entered; } void SetFloat32DenormPreserve(bool enabled) { float32DenormPreserve = enabled; } void SetFloat32FlushToZero(bool enabled) { float32FlushToZero = enabled; } void SetFloat32FlushDeclared(bool enabled) { float32FlushDeclared = enabled; } @@ -59,7 +58,6 @@ private: IrU32 applyBitSourceModifiers(const RdnaOperand& operand, IrU32 value); IrValue* readOperand(const RdnaOperand& operand, IrType type); IrU1 threadBit(const std::array<IrU32, 2>& mask); - IrU1 execLanes(const std::array<IrU32, 2>& mask); void writeRawU32(const RdnaOperand& operand, IrU32 value); IrF32 applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value); IrF32 scaleF32Result(std::uint32_t omod, IrF32 value); @@ -385,7 +383,6 @@ private: RoundingState rounding; bool ieeeMode = false; bool nativeFloat16 = false; - bool wholeQuad = false; IrBuilder ir; IrBlock& block; IrU1 instructionBranchCondition; diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index da01650ed5..bb8094339a 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -45,7 +45,7 @@ void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin) writeRawU32(inst.destination, newCounter); ir.SetExecLo(newExecLo.Value()); ir.SetExecHi(newExecHi.Value()); - ir.SetExec(execLanes({newExecLo, newExecHi}).Value()); + ir.SetExec(threadBit({newExecLo, newExecHi}).Value()); } // s_*_saveexec reads its source before it writes the old exec into the destination, so @@ -84,7 +84,7 @@ void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operati if (writeResult) writeU32Pair(inst.destination, {hostExecWord(0u), hostExecWord(1u)}); const IrU1 nonZero(ir.LogicalOr(ir.INotEqual(newExecLo.Value(), ir.Constant(0u)), ir.INotEqual(newExecHi.Value(), ir.Constant(0u)))); ir.SetScc(nonZero.Value()); - ir.SetExec(execLanes({newExecLo, newExecHi}).Value()); + ir.SetExec(threadBit({newExecLo, newExecHi}).Value()); return; } const IrU32 oldExec(ir.GetExecLo()); @@ -116,7 +116,7 @@ void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operati IrValue& combinedLane = ir.Emit(laneOperation, IrType::U1, {&execLane, &sourceLaneOperand}); newLane = IrU1(negateResult ? ir.LogicalNot(combinedLane) : combinedLane); } else { - newLane = execLanes({newExec, IrU32(ir.GetExecHi())}); + newLane = threadBit({newExec, IrU32(ir.GetExecHi())}); } if (writeResult) writeSaved(hostExecWord(0u), newLane); const IrU1 nonZero(ir.INotEqual(newExec.Value(), ir.Constant(0u))); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index d71a921c64..4a44efe341 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -513,7 +513,6 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con program.SetWaveSize(options.waveSize); std::uint32_t windowBase = NumVectorRegs; bool indexedWrites = false; - bool wholeQuad = false; for (const auto& cfgBlock : cfg.blocks) { for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { const auto& instruction = decoded.instructions[index]; @@ -532,10 +531,6 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con windowBase = std::min({windowBase, instruction.source0.reg, instruction.destination.reg}); indexedWrites = true; break; - case RdnaOpcode::SWqmB32: - case RdnaOpcode::SWqmB64: - wholeQuad = wholeQuad || instruction.destination.kind == RdnaOperandKind::ExecLo || instruction.destination.kind == RdnaOperandKind::ExecHi; - break; default: break; } @@ -624,7 +619,6 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con context.SetPixelInput(options.inputInfo.pixel, options.fragmentShaderBarycentricEnabled); context.SetFloatMode(options.floatMode); context.SetNativeFloat16(options.nativeFloat16); - context.SetWholeQuad(wholeQuad); context.SetFloat32DenormPreserve(options.float32DenormPreserve); context.SetFloat32FlushToZero(options.float32FlushToZero); context.SetFloat32FlushDeclared(options.float32FlushDeclared); diff --git a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp index bc26b8dbb7..5a87cd02e4 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp @@ -15,32 +15,14 @@ IrU32 TranslationContext::hostExecWord(std::uint32_t half) { if (half != 0u && program.WaveSize() != 64u) { return word; } - const auto active = ballotMask(IrU1(ir.ConstantBool(true))); + auto active = ballotMask(IrU1(ir.ConstantBool(true))); + if (program.Resources().stage == IrShaderStage::Pixel) { + const IrU64 lanes(ir.ConstructU64(active[0].Value(), active[1].Value())); + active = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&lanes.Value()}))); + } return IrU32(ir.BitwiseAnd(word.Value(), active[half].Value())); } -IrU1 TranslationContext::execLanes(const std::array<IrU32, 2>& mask) { - if (program.Resources().stage != IrShaderStage::Pixel || !wholeQuad) return threadBit(mask); - if (mask[0].Value().HasImmediate() && mask[1].Value().HasImmediate()) { - const std::uint64_t value = (static_cast<std::uint64_t>(mask[1].Value().ImmediateU32()) << 32u) | mask[0].Value().ImmediateU32(); - std::uint64_t quads = value | (value >> 1u) | (value >> 2u) | (value >> 3u); - quads &= 0x1111111111111111ull; - if (quads * 0xfull == value) return threadBit(mask); - } - IrValue& builtin = ir.Emit(IrOpcode::GetBuiltin, IrOpcodeType(IrOpcode::GetBuiltin), {&ir.Constant(static_cast<std::uint32_t>(StageInputKind::HelperInvocation)), &ir.Constant(0u)}); - const auto live = ballotMask(IrU1(ir.IEqual(builtin, ir.Constant(0u)))); - const IrU64 liveLanes(ir.ConstructU64(live[0].Value(), live[1].Value())); - const auto liveQuads = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&liveLanes.Value()}))); - const IrU64 lanes(ir.ConstructU64(mask[0].Value(), mask[1].Value())); - const auto quads = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&lanes.Value()}))); - std::array<IrU32, 2> words = mask; - for (std::size_t half = 0; half < words.size(); ++half) { - IrValue& helpers = ir.BitwiseAnd(liveQuads[half].Value(), ir.BitwiseNot(live[half].Value())); - words[half] = IrU32(ir.BitwiseOr(mask[half].Value(), ir.BitwiseAnd(quads[half].Value(), helpers))); - } - return threadBit(words); -} - IrU1 TranslationContext::threadBit(const std::array<IrU32, 2>& mask) { const bool wide = program.WaveSize() == 64u; if (mask[0].Value().HasImmediate() && (!wide || mask[1].Value().HasImmediate())) { diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index 2f07f9846c..ebec2355da 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -567,11 +567,11 @@ void TranslationContext::writeRawU32(const RdnaOperand& operand, IrU32 value) { case RdnaOperandKind::M0: ir.SetM0(value.Value()); break; case RdnaOperandKind::ExecLo: ir.SetExecLo(value.Value()); - ir.SetExec(execLanes({value, IrU32(ir.GetExecHi())}).Value()); + ir.SetExec(threadBit({value, IrU32(ir.GetExecHi())}).Value()); break; case RdnaOperandKind::ExecHi: ir.SetExecHi(value.Value()); - ir.SetExec(execLanes({IrU32(ir.GetExecLo()), value}).Value()); + ir.SetExec(threadBit({IrU32(ir.GetExecLo()), value}).Value()); break; case RdnaOperandKind::Scc: ir.SetScc(ir.INotEqual(value.Value(), ir.Constant(0u))); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 6e971a55b5..9bd5810262 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -103,7 +103,7 @@ - [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from PPSA04263's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. -- [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) and every [pixel-stage ballot](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) leave out helper invocations, unlike the console: guest SGPR masks, lane reads and lane counts never include a helper lane, so a waterfall over helper lanes skips them (not measured on a console). Vulkan lets helpers stay out of non-quad subgroup operations, and on an RX 9070 XT they ran no ordinary instructions, so a helper bit in a saved whole-quad EXEC could never be cleared. In a pixel program that runs `s_wqm` on EXEC, writing EXEC from a value keeps a helper active while a live lane of its quad is, for implicit-LOD sampling ([RDNA 2 ISA, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119)). EXEC reads also stay within host lanes; the console holds lanes the host lacks (32-63 of a wave64 on 32 host lanes). +- [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) is limited to host lanes. Pixel reads retain helper bits in quads with an active fragment, preserving whole-quad masks across copies. Quad expansion follows the [RDNA 2 ISA, section 12.3, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119); save/restore followed by implicit-LOD sampling is checked on an RX 6700 XT, not on a console. The console also holds lanes the host lacks, e.g. 32-63 of a wave64 on 32 host lanes. - [`v_readlane_b32`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. - 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) or, in a program whose entry EXEC is every lane (vertex, local and tessellation stages), as 0 when the value is 0 in lanes 32-63; other such reads throw. A value is 0 there when it is built from 0 through selects under a lane index (lane id or `v_mbcnt` of -1) below at most 32, `v_cmp_ne_u32` against 0, OR, DPP and permlanes; the counts in `s3` give such a bound on a 32-lane host. On a 32-lane host a half-wave scan may also run under `s_mov_b64 exec, -1`, use bound_ctrl:1 steps for an OR, add or unsigned max, and take keys that are 0 in lanes 32-63; its lane 31 is then the subgroup reduction of the host's invocations (a shuffle of the scan on a device without subgroup arithmetic): idle invocations add the identity, where the console would add the keys of the lanes `s3` claims. From f5a61dc3c54d601f9bf344de9907ed0b16b52d72 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:21:19 -0300 Subject: [PATCH 702/702] docs: clarify the shader loop guard limit --- docs/dev/ENVIRONMENT.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index b74ec48890..3ee04106fb 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -24,6 +24,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti ### core/shader +`APS5_LOOP_GUARD=<exits>` limits evaluated loop exits per shader invocation (`core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp`); `0` or an unset variable disables that limit. Its presence separately enables the AGC fault buffer, as listed under `libSceAgcDriver`. + | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| | `ANYPS5_NO_SHADER_CACHE` | presence | `core/shader/recompiler/ShaderDiskCache.cpp` | | disables the on-disk shader cache when set to a non-empty value other than 0 |